From 7340e57ddb6458351d9f09893c8763f78edd544c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Wed, 4 Oct 2023 15:08:06 +0200 Subject: [PATCH 1/4] feat: archived conversation list [WPB-4429] (#2295) --- .../android/navigation/HomeDestination.kt | 1 + .../com/wire/android/ui/home/HomeScreen.kt | 9 + .../android/ui/home/archive/ArchiveScreen.kt | 35 +++- .../ConversationListViewModel.kt | 170 +++++++++--------- .../conversationslist/ConversationRouter.kt | 8 +- .../all/AllConversationScreen.kt | 9 +- .../conversationslist/model/SearchQuery.kt | 31 ++++ .../ImportMediaAuthenticatedViewModel.kt | 2 +- .../ConversationListViewModelTest.kt | 86 ++++++++- kalium | 2 +- 10 files changed, 252 insertions(+), 101 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/SearchQuery.kt diff --git a/app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt b/app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt index a501f4685cd..bea848e7b88 100644 --- a/app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt +++ b/app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt @@ -82,6 +82,7 @@ sealed class HomeDestination( data object Archive : HomeDestination( title = R.string.archive_screen_title, icon = R.drawable.ic_archive, + isSearchable = true, direction = ArchiveScreenDestination ) 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 0623cd1a5af..411e95d7f94 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 @@ -85,6 +85,7 @@ import com.wire.android.ui.home.conversations.details.GroupConversationActionTyp import com.wire.android.ui.home.conversations.details.GroupConversationDetailsNavBackArgs import com.wire.android.ui.home.conversationslist.ConversationListState import com.wire.android.ui.home.conversationslist.ConversationListViewModel +import com.wire.android.ui.home.conversationslist.model.ConversationsSource import com.wire.android.ui.home.drawer.HomeDrawer import com.wire.android.ui.home.drawer.HomeDrawerState import com.wire.android.ui.home.drawer.HomeDrawerViewModel @@ -111,6 +112,14 @@ fun HomeScreen( showNotificationsFlow.launch() } + LaunchedEffect(homeScreenState.currentNavigationItem) { + when (homeScreenState.currentNavigationItem) { + HomeDestination.Archive -> conversationListViewModel.updateConversationsSource(ConversationsSource.ARCHIVE) + HomeDestination.Conversations -> conversationListViewModel.updateConversationsSource(ConversationsSource.MAIN) + else -> {} + } + } + handleSnackBarMessage( snackbarHostState = homeScreenState.snackBarHostState, conversationListSnackBarState = homeScreenState.snackbarState, 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 f8525e03c0c..f030bb84151 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 @@ -36,14 +36,41 @@ import com.ramcosta.composedestinations.annotation.Destination import com.wire.android.R import com.wire.android.navigation.HomeNavGraph import com.wire.android.ui.common.dimensions +import com.wire.android.ui.home.HomeStateHolder +import com.wire.android.ui.home.conversationslist.ConversationItemType +import com.wire.android.ui.home.conversationslist.ConversationRouterHomeBridge +import com.wire.android.ui.home.conversationslist.model.ConversationsSource import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography +/** + * ArchiveScreen composable function. + * + * This screen leverages the ConversationRouterHomeBridge to render its UI and logic. + * Reasons for using ConversationRouterHomeBridge: + * 1. **Consistency**: Ensures a uniform UI/UX between the Archive and Conversation screens. + * 2. **Code Efficiency**: Eliminates redundancy by reusing shared logic and components. + * 3. **Flexibility**: Accommodates distinct data queries while retaining core UI logic. + * 4. **Maintainability**: Centralizes updates, reducing potential bugs and inconsistencies. + * 5. **Optimization**: Speeds up the development cycle by reusing established components. + */ @HomeNavGraph @Destination @Composable -fun ArchiveScreen() { - ArchivedConversationsEmptyStateScreen() +fun ArchiveScreen(homeStateHolder: HomeStateHolder) { + with(homeStateHolder) { + ConversationRouterHomeBridge( + navigator = navigator, + conversationItemType = ConversationItemType.ALL_CONVERSATIONS, + onHomeBottomSheetContentChanged = ::changeBottomSheetContent, + onOpenBottomSheet = ::openBottomSheet, + onCloseBottomSheet = ::closeBottomSheet, + onSnackBarStateChanged = ::setSnackBarState, + searchBarState = searchBarState, + isBottomSheetVisible = ::isBottomSheetVisible, + conversationsSource = ConversationsSource.ARCHIVE + ) + } } @Composable @@ -78,6 +105,6 @@ fun ArchivedConversationsEmptyStateScreen() { @Preview(showBackground = false) @Composable -fun PreviewArchiveScreen() { - ArchiveScreen() +fun PreviewArchiveEmptyScreen() { + ArchivedConversationsEmptyStateScreen() } 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 b9d3848d116..f8ec9dcb98d 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 @@ -41,8 +41,11 @@ import com.wire.android.ui.home.conversationslist.model.BlockState import com.wire.android.ui.home.conversationslist.model.ConversationFolder import com.wire.android.ui.home.conversationslist.model.ConversationInfo import com.wire.android.ui.home.conversationslist.model.ConversationItem +import com.wire.android.ui.home.conversationslist.model.ConversationsSource import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.home.conversationslist.model.GroupDialogState +import com.wire.android.ui.home.conversationslist.model.SearchQuery +import com.wire.android.ui.home.conversationslist.model.SearchQueryUpdate import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.conversation.ConversationDetails @@ -78,17 +81,17 @@ import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusU import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase import com.wire.kalium.logic.feature.team.Result -import com.wire.kalium.logic.functional.combine import com.wire.kalium.util.DateTimeUtil import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.scan import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.Date @@ -123,10 +126,24 @@ class ConversationListViewModel @Inject constructor( var requestInProgress: Boolean by mutableStateOf(false) - private val mutableSearchQueryFlow = MutableStateFlow("") + private val mutableSearchQueryFlow = MutableSharedFlow() private val searchQueryFlow = mutableSearchQueryFlow - .asStateFlow() + .scan(SearchQuery("", ConversationsSource.MAIN)) { currentSearchQuery, update -> + when (update) { + is SearchQueryUpdate.UpdateQuery -> currentSearchQuery.copy(text = update.text) + is SearchQueryUpdate.UpdateConversationsSource -> { + if (currentSearchQuery.source != update.source) { + currentSearchQuery.copy( + text = "", + source = update.source + ) + } else { + currentSearchQuery + } + } + } + } .debounce(SearchPeopleViewModel.DEFAULT_SEARCH_QUERY_DEBOUNCE) var establishedCallConversationId: QualifiedID? = null @@ -151,8 +168,8 @@ class ConversationListViewModel @Inject constructor( observeEstablishedCall() } viewModelScope.launch { - searchQueryFlow.combine( - observeConversationListDetails(includeArchived = false) + searchQueryFlow.flatMapLatest { searchQuery -> + observeConversationListDetails(fromArchive = searchQuery.source == ConversationsSource.ARCHIVE) .map { it.map { conversationDetails -> conversationDetails.toConversationItem( @@ -161,24 +178,30 @@ class ConversationListViewModel @Inject constructor( ) } } - ) - .map { (searchQuery, conversationItems) -> conversationItems.withFolders().toImmutableMap() to searchQuery } - .collect { (conversationsWithFolders, searchQuery) -> - conversationListState = conversationListState.copy( - conversationSearchResult = if (searchQuery.isEmpty()) { + .map { conversationItems -> + conversationItems.withFolders(source = searchQuery.source) + .toImmutableMap() to searchQuery + } + } + .map { (conversationsWithFolders, searchQuery) -> + conversationListState.copy( + conversationSearchResult = if (searchQuery.text.isEmpty()) { conversationsWithFolders } else { searchConversation( conversationsWithFolders.values.flatten(), - searchQuery - ).withFolders().toImmutableMap() + searchQuery.text + ).withFolders(source = searchQuery.source).toImmutableMap() }, hasNoConversations = conversationsWithFolders.isEmpty(), foldersWithConversations = conversationsWithFolders, - // TODO: missing other lists and counters (for bottom tabs if we decide to bring them back) - searchQuery = searchQuery + searchQuery = searchQuery.text ) } + .flowOn(dispatcher.io()) + .collect { + conversationListState = it + } } } @@ -211,77 +234,48 @@ class ConversationListViewModel @Inject constructor( } @Suppress("ComplexMethod") - private fun List.withFolders(): Map> { - val unreadConversations = filter { - when (it.mutedStatus) { - MutedConversationStatus.AllAllowed -> when (it.badgeEventType) { - BadgeEventType.Blocked -> false - BadgeEventType.Deleted -> false - BadgeEventType.Knock -> true - BadgeEventType.MissedCall -> true - BadgeEventType.None -> false - BadgeEventType.ReceivedConnectionRequest -> true - BadgeEventType.SentConnectRequest -> false - BadgeEventType.UnreadMention -> true - is BadgeEventType.UnreadMessage -> true - BadgeEventType.UnreadReply -> true - } - - MutedConversationStatus.OnlyMentionsAndRepliesAllowed -> when (it.badgeEventType) { - BadgeEventType.UnreadReply -> true - BadgeEventType.UnreadMention -> true - BadgeEventType.ReceivedConnectionRequest -> true - else -> false + private fun List.withFolders(source: ConversationsSource): Map> { + return when (source) { + ConversationsSource.ARCHIVE -> { + buildMap { + if (this@withFolders.isNotEmpty()) put(ConversationFolder.WithoutHeader, this@withFolders) } + } - MutedConversationStatus.AllMuted -> false - } || (it is ConversationItem.GroupConversation && it.hasOnGoingCall) - } - - val remainingConversations = this - unreadConversations.toSet() - - return buildMap { - if (unreadConversations.isNotEmpty()) put(ConversationFolder.Predefined.NewActivities, unreadConversations) - if (remainingConversations.isNotEmpty()) put(ConversationFolder.Predefined.Conversations, remainingConversations) - } - } - - @Suppress("ComplexMethod", "NoMultipleSpaces") - private fun List.toConversationsFoldersMap(): Map> { - val unreadConversations = filter { - when (it.conversation.mutedStatus) { - MutedConversationStatus.AllAllowed -> - when (it) { - is Group -> it.unreadEventCount.isNotEmpty() - is OneOne -> it.unreadEventCount.isNotEmpty() - else -> false // TODO should connection requests also be listed on "new activities"? - } - - MutedConversationStatus.OnlyMentionsAndRepliesAllowed -> - when (it) { - is Group -> it.unreadEventCount.containsKey(UnreadEventType.MENTION) || - it.unreadEventCount.containsKey(UnreadEventType.REPLY) - - is OneOne -> it.unreadEventCount.containsKey(UnreadEventType.MENTION) || - it.unreadEventCount.containsKey(UnreadEventType.REPLY) - - else -> false - } + ConversationsSource.MAIN -> { + val unreadConversations = filter { + when (it.mutedStatus) { + MutedConversationStatus.AllAllowed -> when (it.badgeEventType) { + BadgeEventType.Blocked -> false + BadgeEventType.Deleted -> false + BadgeEventType.Knock -> true + BadgeEventType.MissedCall -> true + BadgeEventType.None -> false + BadgeEventType.ReceivedConnectionRequest -> true + BadgeEventType.SentConnectRequest -> false + BadgeEventType.UnreadMention -> true + is BadgeEventType.UnreadMessage -> true + BadgeEventType.UnreadReply -> true + } - else -> false - } || - (it is Connection && it.connection.status == ConnectionState.PENDING) || - (it is Group && it.hasOngoingCall) - } + MutedConversationStatus.OnlyMentionsAndRepliesAllowed -> when (it.badgeEventType) { + BadgeEventType.UnreadReply -> true + BadgeEventType.UnreadMention -> true + BadgeEventType.ReceivedConnectionRequest -> true + else -> false + } - val remainingConversations = this - unreadConversations.toSet() + MutedConversationStatus.AllMuted -> false + } || (it is ConversationItem.GroupConversation && it.hasOnGoingCall) + } - val unreadConversationsItems = unreadConversations.toConversationItemList() - val remainingConversationsItems = remainingConversations.toConversationItemList() + val remainingConversations = this - unreadConversations.toSet() - return buildMap { - if (unreadConversationsItems.isNotEmpty()) put(ConversationFolder.Predefined.NewActivities, unreadConversationsItems) - if (remainingConversationsItems.isNotEmpty()) put(ConversationFolder.Predefined.Conversations, remainingConversationsItems) + buildMap { + if (unreadConversations.isNotEmpty()) put(ConversationFolder.Predefined.NewActivities, unreadConversations) + if (remainingConversations.isNotEmpty()) put(ConversationFolder.Predefined.Conversations, remainingConversations) + } + } } } @@ -399,15 +393,15 @@ class ConversationListViewModel @Inject constructor( } } - private fun List.toConversationItemList(): List = - filter { it is Group || it is OneOne || it is Connection } - .map { - it.toConversationItem(wireSessionImageLoader, userTypeMapper) - } - fun searchConversation(searchQuery: TextFieldValue) { viewModelScope.launch { - mutableSearchQueryFlow.emit(searchQuery.text) + mutableSearchQueryFlow.emit(SearchQueryUpdate.UpdateQuery(searchQuery.text)) + } + } + + fun updateConversationsSource(source: ConversationsSource) { + viewModelScope.launch { + mutableSearchQueryFlow.emit(SearchQueryUpdate.UpdateConversationsSource(source)) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt index 3f7bb88a97d..7709e7f7b17 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt @@ -55,6 +55,7 @@ import com.wire.android.ui.home.conversationslist.all.AllConversationScreenConte import com.wire.android.ui.home.conversationslist.call.CallsScreenContent import com.wire.android.ui.home.conversationslist.mention.MentionScreenContent import com.wire.android.ui.home.conversationslist.model.ConversationItem +import com.wire.android.ui.home.conversationslist.model.ConversationsSource import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.home.conversationslist.model.GroupDialogState import com.wire.android.ui.home.conversationslist.search.SearchConversationScreen @@ -75,11 +76,16 @@ fun ConversationRouterHomeBridge( onCloseBottomSheet: () -> Unit, onSnackBarStateChanged: (HomeSnackbarState) -> Unit, searchBarState: SearchBarState, - isBottomSheetVisible: () -> Boolean + isBottomSheetVisible: () -> Boolean, + conversationsSource: ConversationsSource = ConversationsSource.MAIN ) { val viewModel: ConversationListViewModel = hiltViewModel() val context = LocalContext.current + LaunchedEffect(conversationsSource) { + viewModel.updateConversationsSource(conversationsSource) + } + MicrophoneBTPermissionsDeniedDialog( shouldShow = viewModel.conversationListState.shouldShowCallingPermissionDialog, onDismiss = viewModel::dismissCallingPermissionDialog, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationScreen.kt index e090d438ef5..5fbd63c1824 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationScreen.kt @@ -45,6 +45,7 @@ import com.wire.android.navigation.HomeNavGraph import com.wire.android.ui.common.dialogs.calling.JoinAnywayDialog import com.wire.android.ui.common.dimensions import com.wire.android.ui.home.HomeStateHolder +import com.wire.android.ui.home.archive.ArchivedConversationsEmptyStateScreen import com.wire.android.ui.home.conversationslist.ConversationItemType import com.wire.android.ui.home.conversationslist.ConversationListViewModel import com.wire.android.ui.home.conversationslist.ConversationRouterHomeBridge @@ -80,6 +81,7 @@ fun AllConversationScreen(homeStateHolder: HomeStateHolder) { fun AllConversationScreenContent( conversations: ImmutableMap>, hasNoConversations: Boolean, + isFromArchive: Boolean = false, viewModel: ConversationListViewModel = hiltViewModel(), onEditConversation: (ConversationItem) -> Unit, onOpenConversationNotificationsSettings: (ConversationItem) -> Unit, @@ -99,7 +101,11 @@ fun AllConversationScreenContent( ) } if (hasNoConversations) { - ConversationListEmptyStateScreen() + if (isFromArchive) { + ArchivedConversationsEmptyStateScreen() + } else { + ConversationListEmptyStateScreen() + } } else { ConversationList( lazyListState = lazyListState, @@ -154,6 +160,7 @@ fun ConversationListEmptyStateScreen() { ) } } + @Preview @Composable fun PreviewAllConversationScreen() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/SearchQuery.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/SearchQuery.kt new file mode 100644 index 00000000000..549df639d93 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/SearchQuery.kt @@ -0,0 +1,31 @@ +/* + * Wire + * Copyright (C) 2023 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.conversationslist.model + +data class SearchQuery(val text: String, val source: ConversationsSource) + +sealed class SearchQueryUpdate { + data class UpdateQuery(val text: String) : SearchQueryUpdate() + + data class UpdateConversationsSource(val source: ConversationsSource) : SearchQueryUpdate() +} + +enum class ConversationsSource { + MAIN, + ARCHIVE +} diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt index 327d81e7188..e76814f5cfb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt @@ -119,7 +119,7 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) private suspend fun observeConversationWithSearch() = viewModelScope.launch { searchQueryFlow.mapLatest { searchQuery -> - val conversations = observeConversationListDetails(includeArchived = false).first() + val conversations = observeConversationListDetails(fromArchive = false).first() .mapNotNull { conversationDetails -> conversationDetails.toConversationItem( wireSessionImageLoader, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt index 959f23049ab..6bf139efa3a 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt @@ -20,10 +20,12 @@ package com.wire.android.ui.home.conversationslist +import androidx.compose.ui.text.input.TextFieldValue import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri +import com.wire.android.framework.TestConversationDetails import com.wire.android.mapper.UserTypeMapper import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.dialogs.BlockUserDialogState @@ -31,9 +33,12 @@ import com.wire.android.ui.home.HomeSnackbarState import com.wire.android.ui.home.conversations.model.UILastMessageContent import com.wire.android.ui.home.conversationslist.model.BadgeEventType import com.wire.android.ui.home.conversationslist.model.BlockingState +import com.wire.android.ui.home.conversationslist.model.ConversationFolder import com.wire.android.ui.home.conversationslist.model.ConversationInfo import com.wire.android.ui.home.conversationslist.model.ConversationItem +import com.wire.android.ui.home.conversationslist.model.ConversationsSource import com.wire.android.ui.home.conversationslist.model.Membership +import com.wire.android.util.orDefault import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.id.ConversationId @@ -60,12 +65,15 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK import io.mockk.verify +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.shouldBeEqualTo -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -74,7 +82,7 @@ import org.junit.jupiter.api.extension.ExtendWith // TODO write more tests class ConversationListViewModelTest { - private lateinit var conversationListViewModel: ConversationListViewModel + private var conversationListViewModel: ConversationListViewModel @MockK lateinit var updateConversationMutedStatus: UpdateConversationMutedStatusUseCase @@ -121,12 +129,20 @@ class ConversationListViewModelTest { @MockK(relaxed = true) private lateinit var onJoined: (ConversationId) -> Unit - @BeforeEach - fun setUp() { + private val dispatcher = StandardTestDispatcher() + + init { MockKAnnotations.init(this, relaxUnitFun = true) + Dispatchers.setMain(dispatcher) coEvery { observeEstablishedCalls.invoke() } returns emptyFlow() - coEvery { observeConversationListDetailsUseCase.invoke(any()) } returns emptyFlow() + coEvery { observeConversationListDetailsUseCase.invoke(false) } returns flowOf( + listOf( + TestConversationDetails.CONNECTION, + TestConversationDetails.CONVERSATION_ONE_ONE, + TestConversationDetails.GROUP + ) + ) mockUri() conversationListViewModel = @@ -150,6 +166,64 @@ class ConversationListViewModelTest { ) } + @Test + fun `given empty search query, when collecting, then update state with all conversations`() = runTest { + // Given + val searchQueryText = "" + + // When + dispatcher.scheduler.advanceUntilIdle() + conversationListViewModel.searchConversation(TextFieldValue(searchQueryText)) + dispatcher.scheduler.advanceUntilIdle() + + // Then + assertEquals( + 3, + conversationListViewModel.conversationListState.conversationSearchResult[ConversationFolder.Predefined.Conversations]?.size, + ) + assertEquals(searchQueryText, conversationListViewModel.conversationListState.searchQuery) + } + + @Test + fun `given non-empty search query, when collecting, then update state with filtered conversations`() = runTest { + // Given + val searchQueryText = TestConversationDetails.CONVERSATION_ONE_ONE.conversation.name.orDefault("test") + + // When + dispatcher.scheduler.advanceUntilIdle() + conversationListViewModel.searchConversation(TextFieldValue(searchQueryText)) + dispatcher.scheduler.advanceUntilIdle() + + // Then + assertEquals( + 1, + conversationListViewModel.conversationListState.conversationSearchResult[ConversationFolder.Predefined.Conversations]?.size, + ) + assertEquals(searchQueryText, conversationListViewModel.conversationListState.searchQuery) + } + + @Test + fun `given empty search query, when collecting archived conversations, then update state with only archived conversations`() = runTest { + // Given + coEvery { observeConversationListDetailsUseCase.invoke(true) } returns flowOf( + listOf( + TestConversationDetails.CONVERSATION_ONE_ONE, + TestConversationDetails.GROUP + ) + ) + // When + dispatcher.scheduler.advanceUntilIdle() + conversationListViewModel.updateConversationsSource(ConversationsSource.ARCHIVE) + dispatcher.scheduler.advanceUntilIdle() + + // Then + assertEquals( + 2, + conversationListViewModel.conversationListState.conversationSearchResult[ConversationFolder.WithoutHeader]?.size, + ) + coVerify(exactly = 1) { observeConversationListDetailsUseCase.invoke(true) } + } + @Test fun `given a valid conversation muting state, when calling muteConversation, then should call with call the UseCase`() = runTest { coEvery { updateConversationMutedStatus(any(), any(), any()) } returns ConversationUpdateStatusResult.Success @@ -284,6 +358,8 @@ class ConversationListViewModelTest { private val conversationId = ConversationId("some_id", "some_domain") private val userId: UserId = UserId("someUser", "some_domain") + private val testConversations = TestConversationDetails.CONVERSATION_ONE_ONE + private val conversationItem = ConversationItem.PrivateConversation( userAvatarData = UserAvatarData(), conversationInfo = ConversationInfo( diff --git a/kalium b/kalium index 5bb647a5249..751cd750a5e 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 5bb647a52490b16e4995af8d88b554c6d7b252a4 +Subproject commit 751cd750a5e0430a9556369336a5de1168065d9b From 3522b4a676846f36055d3d0623290141ad877ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Wed, 4 Oct 2023 16:20:54 +0200 Subject: [PATCH 2/4] fix: toast crash [WPB-4937] (#2293) Co-authored-by: Yamil Medina --- .../com/wire/android/ui/WireActivity.kt | 2 +- .../ui/common/scaffold/WireScaffold.kt | 24 ++-- .../snackbar/LocalSnackbarHostState.kt | 2 +- .../common/snackbar/SwipeDismissSnackbar.kt | 82 ------------- .../ui/common/snackbar/SwipeableSnackbar.kt | 113 ++++++++++++++++++ .../ui/{ => common}/snackbar/WireSnackbar.kt | 2 +- .../ui/connection/ConnectionActionButton.kt | 4 +- .../wire/android/ui/home/HomeStateHolder.kt | 2 +- .../home/conversations/ConversationScreen.kt | 18 +-- .../conversations/ConversationScreenState.kt | 2 +- .../details/GroupConversationDetailsScreen.kt | 2 +- .../editguestaccess/EditGuestAccessScreen.kt | 2 +- .../home/gallery/MediaGalleryScreenState.kt | 2 +- .../recordaudio/RecordAudioComponent.kt | 2 +- .../NewConversationSearchPeopleScreen.kt | 2 +- .../home/settings/account/MyAccountScreen.kt | 2 +- .../devices/DeviceDetailsScreenState.kt | 2 +- .../ui/sharing/ImportMediaScreenState.kt | 2 +- .../avatarpicker/AvatarPickerState.kt | 2 +- .../other/OtherUserProfileScreen.kt | 2 +- .../other/OtherUserProfileScreenState.kt | 2 +- .../userprofile/self/SelfUserProfileScreen.kt | 2 +- .../service/ServiceDetailsScreen.kt | 2 +- 23 files changed, 161 insertions(+), 116 deletions(-) rename app/src/main/kotlin/com/wire/android/ui/{ => common}/snackbar/LocalSnackbarHostState.kt (95%) delete mode 100644 app/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeDismissSnackbar.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeableSnackbar.kt rename app/src/main/kotlin/com/wire/android/ui/{ => common}/snackbar/WireSnackbar.kt (96%) 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 4c5557b3831..7a5367357d2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -79,7 +79,7 @@ import com.wire.android.ui.destinations.WelcomeScreenDestination import com.wire.android.ui.home.E2EIRequiredDialog import com.wire.android.ui.home.E2EISnoozeDialog import com.wire.android.ui.home.sync.FeatureFlagNotificationViewModel -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.theme.WireTheme import com.wire.android.util.CurrentScreenManager import com.wire.android.util.LocalSyncStateObserver diff --git a/app/src/main/kotlin/com/wire/android/ui/common/scaffold/WireScaffold.kt b/app/src/main/kotlin/com/wire/android/ui/common/scaffold/WireScaffold.kt index ce1f7385de6..5577006d118 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/scaffold/WireScaffold.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/scaffold/WireScaffold.kt @@ -17,6 +17,7 @@ */ package com.wire.android.ui.common.scaffold +import SwipeableSnackbar import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.imePadding @@ -24,12 +25,12 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.FabPosition import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import com.wire.android.ui.common.snackbar.SwipeDismissSnackbarHost -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState /** * A custom scaffold that automatically applies system UI insets and IME (Input Method Editor) @@ -68,12 +69,21 @@ fun WireScaffold( topBar = topBar, bottomBar = bottomBar, snackbarHost = { - SwipeDismissSnackbarHost(hostState = LocalSnackbarHostState.current) + SnackbarHost( + hostState = LocalSnackbarHostState.current, + snackbar = { data -> + SwipeableSnackbar( + hostState = LocalSnackbarHostState.current, + data = data, + onDismiss = { data.dismiss() } + ) + } + ) }, - floatingActionButton, - floatingActionButtonPosition, - containerColor, - contentColor, + floatingActionButton = floatingActionButton, + floatingActionButtonPosition = floatingActionButtonPosition, + containerColor = containerColor, + contentColor = contentColor, contentWindowInsets = WindowInsets(0, 0, 0, 0), content = content ) diff --git a/app/src/main/kotlin/com/wire/android/ui/snackbar/LocalSnackbarHostState.kt b/app/src/main/kotlin/com/wire/android/ui/common/snackbar/LocalSnackbarHostState.kt similarity index 95% rename from app/src/main/kotlin/com/wire/android/ui/snackbar/LocalSnackbarHostState.kt rename to app/src/main/kotlin/com/wire/android/ui/common/snackbar/LocalSnackbarHostState.kt index f01665669e8..b2564ebc9e7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/snackbar/LocalSnackbarHostState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/snackbar/LocalSnackbarHostState.kt @@ -15,7 +15,7 @@ * 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.snackbar +package com.wire.android.ui.common.snackbar import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.staticCompositionLocalOf diff --git a/app/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeDismissSnackbar.kt b/app/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeDismissSnackbar.kt deleted file mode 100644 index 7efb757bc7c..00000000000 --- a/app/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeDismissSnackbar.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Wire - * Copyright (C) 2023 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - * - */ - -package com.wire.android.ui.common.snackbar - -import androidx.compose.material3.DismissDirection -import androidx.compose.material3.DismissValue -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SwipeToDismiss -import androidx.compose.material3.rememberDismissState -import androidx.compose.material3.Snackbar -import androidx.compose.material3.SnackbarData -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -/** - * Wrapper around [Snackbar] to make it swipe-dismissable, - * using [SwipeToDismiss]. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SwipeDismissSnackbar( - data: SnackbarData, - onDismiss: (() -> Unit)? = null, - snackbar: @Composable (SnackbarData) -> Unit = { Snackbar(it) }, -) { - val dismissState = rememberDismissState( - confirmValueChange = { - if (it != DismissValue.Default) { - // First dismiss the snackbar - data.dismiss() - // Then invoke the callback - onDismiss?.invoke() - } - true - }) - - SwipeToDismiss( - state = dismissState, - directions = setOf(DismissDirection.StartToEnd, DismissDirection.EndToStart), - background = {}, - dismissContent = { snackbar(data) } - ) -} - -@Composable -fun SwipeDismissSnackbarHost( - hostState: SnackbarHostState, - modifier: Modifier = Modifier, - onDismiss: () -> Unit = { hostState.currentSnackbarData?.dismiss() }, - snackbar: @Composable (SnackbarData) -> Unit = { data -> - SwipeDismissSnackbar( - data = data, - onDismiss = onDismiss, - ) - }, -) { - SnackbarHost( - hostState = hostState, - snackbar = snackbar, - modifier = modifier, - ) -} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeableSnackbar.kt b/app/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeableSnackbar.kt new file mode 100644 index 00000000000..9efc24c06ec --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeableSnackbar.kt @@ -0,0 +1,113 @@ +/* + * Wire + * Copyright (C) 2023 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/. + */ + +import androidx.compose.animation.core.SpringSpec +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.layout.offset +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarData +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt + +/** + * A swipeable [Snackbar] that allows users to manually dismiss it by dragging. + * + * This composable function extends the default Snackbar behavior by adding a draggable gesture. + * The Snackbar can be swiped horizontally to dismiss it, based on predefined positional and velocity thresholds. + * + * @param hostState The state of the [SnackbarHostState] this Snackbar is associated with. This allows + * the Snackbar to notify its host when it's dismissed. + * @param data The [SnackbarData] containing the message and optional action to display on the Snackbar. + * @param onDismiss An optional callback function to be executed when the Snackbar is swiped away. + * The default behavior will dismiss the current Snackbar from the [hostState]. + * @see Snackbar + * @see SnackbarData + * @see SnackbarHostState + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun SwipeableSnackbar( + hostState: SnackbarHostState, + data: SnackbarData, + onDismiss: () -> Unit = { hostState.currentSnackbarData?.dismiss() }, +) { + val density = LocalDensity.current + val configuration = LocalConfiguration.current + + val currentScreenWidth = with(density) { configuration.screenWidthDp.dp.toPx() } + + val anchors = DraggableAnchors { + SnackBarState.Visible at 0f + SnackBarState.Dismissed at currentScreenWidth + } + + // Determines how far the user needs to drag (as a fraction of total distance) for an action to be triggered. + // In this example, the Snackbar will trigger an action if dragged to half (0.5) of its width. + val positionalThreshold: (Float) -> Float = { distance -> distance * 0.5f } + + // Determines the minimum velocity (in pixels per second) with which the user needs to drag for an action to be triggered, + // even if the positional threshold hasn't been reached. + // Here, it's set to 125 device-independent pixels per second. + val velocityThreshold: () -> Float = with(density) { { 125.dp.toPx() } } + + val state = remember { + AnchoredDraggableState( + initialValue = SnackBarState.Visible, + anchors = anchors, + positionalThreshold = positionalThreshold, + velocityThreshold = velocityThreshold, + animationSpec = SpringSpec(), + confirmValueChange = { true } + ) + } + + LaunchedEffect(state.currentValue) { + if (state.currentValue == SnackBarState.Dismissed) { + onDismiss() + } + } + + Snackbar( + snackbarData = data, + modifier = Modifier + .anchoredDraggable( + state = state, + orientation = Orientation.Horizontal + ) + .offset { + IntOffset( + state + .requireOffset() + .roundToInt(), 0 + ) + }) +} + +private enum class SnackBarState { Visible, Dismissed } diff --git a/app/src/main/kotlin/com/wire/android/ui/snackbar/WireSnackbar.kt b/app/src/main/kotlin/com/wire/android/ui/common/snackbar/WireSnackbar.kt similarity index 96% rename from app/src/main/kotlin/com/wire/android/ui/snackbar/WireSnackbar.kt rename to app/src/main/kotlin/com/wire/android/ui/common/snackbar/WireSnackbar.kt index b0f4914c491..1960e866eee 100644 --- a/app/src/main/kotlin/com/wire/android/ui/snackbar/WireSnackbar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/snackbar/WireSnackbar.kt @@ -15,7 +15,7 @@ * 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.snackbar +package com.wire.android.ui.common.snackbar import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable diff --git a/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButton.kt b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButton.kt index a9a82ace49e..421b111a465 100644 --- a/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButton.kt @@ -39,8 +39,8 @@ import com.wire.android.ui.common.dialogs.UnblockUserDialogContent import com.wire.android.ui.common.dialogs.UnblockUserDialogState import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.visbility.rememberVisibilityState -import com.wire.android.ui.snackbar.LocalSnackbarHostState -import com.wire.android.ui.snackbar.collectAndShowSnackbar +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.collectAndShowSnackbar import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeStateHolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeStateHolder.kt index 179fd36adaa..3667123bea0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeStateHolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeStateHolder.kt @@ -44,7 +44,7 @@ import com.wire.android.ui.common.bottomsheet.WireModalSheetState import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.topappbar.search.SearchBarState import com.wire.android.ui.common.topappbar.search.rememberSearchbarState -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 85917834032..3bf769113a3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -20,16 +20,17 @@ package com.wire.android.ui.home.conversations +import SwipeableSnackbar import android.net.Uri import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -73,7 +74,7 @@ import com.wire.android.ui.common.dialogs.calling.ConfirmStartCallDialog import com.wire.android.ui.common.dialogs.calling.JoinAnywayDialog import com.wire.android.ui.common.dialogs.calling.OngoingActiveCallDialog import com.wire.android.ui.common.error.CoreFailureErrorDialog -import com.wire.android.ui.common.snackbar.SwipeDismissSnackbarHost +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.destinations.GroupConversationDetailsScreenDestination import com.wire.android.ui.destinations.InitiatingCallScreenDestination import com.wire.android.ui.destinations.MediaGalleryScreenDestination @@ -104,7 +105,6 @@ import com.wire.android.ui.home.messagecomposer.MessageComposer import com.wire.android.ui.home.messagecomposer.state.MessageBundle import com.wire.android.ui.home.messagecomposer.state.MessageComposerStateHolder import com.wire.android.ui.home.messagecomposer.state.rememberMessageComposerStateHolder -import com.wire.android.ui.snackbar.LocalSnackbarHostState import com.wire.android.util.extension.openAppInfoScreen import com.wire.android.util.normalizeLink import com.wire.android.util.ui.UIText @@ -570,11 +570,15 @@ private fun ConversationScreen( } }, snackbarHost = { - SwipeDismissSnackbarHost( + SnackbarHost( hostState = snackbarHostState, - modifier = Modifier - .fillMaxWidth() - .imePadding() + snackbar = { data -> + SwipeableSnackbar( + hostState = snackbarHostState, + data = data, + onDismiss = { data.dismiss() } + ) + } ) }, content = { internalPadding -> diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreenState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreenState.kt index c4c561a41eb..247395a42d6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreenState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreenState.kt @@ -37,7 +37,7 @@ import com.wire.android.R import com.wire.android.ui.common.bottomsheet.WireModalSheetState import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.home.conversations.model.UIMessage -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch 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 2e9f5925506..a9ed4949afa 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 @@ -92,7 +92,7 @@ import com.wire.android.ui.home.conversations.details.participants.GroupConversa import com.wire.android.ui.home.conversations.details.participants.model.UIParticipant import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.home.conversationslist.model.GroupDialogState -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.util.ui.UIText diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessScreen.kt index 5127dd8b869..a635def14b9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessScreen.kt @@ -57,7 +57,7 @@ import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.destinations.CreatePasswordProtectedGuestLinkScreenDestination import com.wire.android.ui.home.conversations.details.editguestaccess.createPasswordProtectedGuestLink.CreatePasswordGuestLinkNavArgs import com.wire.android.ui.home.conversationslist.common.FolderHeader -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography diff --git a/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryScreenState.kt b/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryScreenState.kt index 0828953ce8d..da3819c8e34 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryScreenState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryScreenState.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import com.wire.android.ui.common.bottomsheet.WireModalSheetState import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioComponent.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioComponent.kt index b9898bb468f..9cf56e59809 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioComponent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioComponent.kt @@ -38,7 +38,7 @@ import com.sebaslogen.resaca.hilt.hiltViewModelScoped import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.home.conversations.model.UriAsset -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.theme.wireColorScheme import com.wire.android.util.extension.openAppInfoScreen import com.wire.android.util.permission.rememberRecordAudioRequestFlow diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/search/NewConversationSearchPeopleScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/search/NewConversationSearchPeopleScreen.kt index 74e9b6c5347..bd762d7920e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/search/NewConversationSearchPeopleScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/search/NewConversationSearchPeopleScreen.kt @@ -33,7 +33,7 @@ import com.wire.android.ui.home.conversations.search.SearchPeopleScreen import com.wire.android.ui.home.conversations.search.SearchPeopleScreenType import com.wire.android.ui.home.newconversation.NewConversationViewModel import com.wire.android.ui.home.newconversation.common.NewConversationNavGraph -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.kalium.logic.data.id.QualifiedID @NewConversationNavGraph(start = true) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountScreen.kt index da32aafa5b3..5754fa0240c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountScreen.kt @@ -67,7 +67,7 @@ import com.wire.android.ui.home.settings.account.AccountDetailsItem.Team import com.wire.android.ui.home.settings.account.AccountDetailsItem.Username import com.wire.android.ui.home.settings.account.deleteAccount.DeleteAccountDialog import com.wire.android.ui.home.settings.account.deleteAccount.DeleteAccountViewModel -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography import com.wire.android.util.CustomTabsHelper diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreenState.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreenState.kt index 2823002bca6..bf64f32f302 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreenState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreenState.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import com.wire.android.R -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreenState.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreenState.kt index 55ec67d2f97..3a5a5a686f4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreenState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreenState.kt @@ -25,7 +25,7 @@ import com.wire.android.ui.common.bottomsheet.WireModalSheetState import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.topappbar.search.SearchBarState import com.wire.android.ui.common.topappbar.search.rememberSearchbarState -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerState.kt index bd089a03ea6..bf7275daf8d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerState.kt @@ -32,7 +32,7 @@ import com.wire.android.ui.common.bottomsheet.WireModalSheetState import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.imagepreview.AvatarPickerFlow import com.wire.android.ui.common.imagepreview.rememberPickPictureState -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.util.ui.UIText import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch 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 53657638244..3ab61983037 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 @@ -90,7 +90,7 @@ import com.wire.android.ui.destinations.DeviceDetailsScreenDestination import com.wire.android.ui.home.conversations.details.dialog.ClearConversationContentDialog import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.home.conversationslist.model.Membership -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenState.kt index d0bad197711..9266d6016e0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenState.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.AnnotatedString import com.wire.android.R -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch 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 9a01809b62b..67988735a85 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 @@ -81,7 +81,7 @@ import com.wire.android.ui.home.conversations.search.HighlightName import com.wire.android.ui.home.conversations.search.HighlightSubtitle import com.wire.android.ui.home.conversationslist.common.FolderHeader import com.wire.android.ui.theme.WireTheme -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.userprofile.common.EditableState import com.wire.android.ui.userprofile.common.UserProfileInfo diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsScreen.kt index 9f6b8f1e3cc..d38227d4f05 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsScreen.kt @@ -32,7 +32,7 @@ import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.home.conversationslist.model.Membership -import com.wire.android.ui.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography From 013245e222c08ab007b965ef0cbfacbed82f5898 Mon Sep 17 00:00:00 2001 From: Gonzo Date: Thu, 5 Oct 2023 12:00:14 +0200 Subject: [PATCH 3/4] feat: add confirmation dialog archiving [WPB-4434] (#2300) --- .../dialogs/ArchiveConversationDialog.kt | 50 +++++++++++++++++++ .../details/GroupConversationDetailsScreen.kt | 22 +++++--- .../GroupConversationDetailsViewModel.kt | 7 ++- ...ersationDetailsBottomSheetEventsHandler.kt | 6 +-- .../ConversationListViewModel.kt | 45 +++++++---------- .../conversationslist/ConversationRouter.kt | 20 +++++--- .../other/OtherUserProfileScreen.kt | 3 ++ .../OtherUserProfileBottomSheet.kt | 10 ++-- app/src/main/res/values/strings.xml | 4 ++ .../GroupConversationDetailsViewModelTest.kt | 36 ++++++++++--- .../ConversationListViewModelTest.kt | 38 ++++++++++++-- kalium | 2 +- 12 files changed, 178 insertions(+), 65 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/dialogs/ArchiveConversationDialog.kt diff --git a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/ArchiveConversationDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/ArchiveConversationDialog.kt new file mode 100644 index 00000000000..c5b5255c213 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/ArchiveConversationDialog.kt @@ -0,0 +1,50 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.common.dialogs + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.wire.android.R +import com.wire.android.ui.common.VisibilityState +import com.wire.android.ui.common.WireDialog +import com.wire.android.ui.common.WireDialogButtonProperties +import com.wire.android.ui.common.WireDialogButtonType +import com.wire.android.ui.common.visbility.VisibilityState +import com.wire.android.ui.home.conversationslist.model.DialogState + +@Composable +fun ArchiveConversationDialog(onArchiveButtonClicked: (DialogState) -> Unit, dialogState: VisibilityState) { + VisibilityState(dialogState) { + WireDialog( + title = stringResource(R.string.dialog_archive_conversation_title), + text = stringResource(R.string.dialog_archive_conversation_description), + onDismiss = dialogState::dismiss, + optionButton1Properties = WireDialogButtonProperties( + onClick = { onArchiveButtonClicked(it) }, + text = stringResource(R.string.dialog_archive_conversation_option), + type = WireDialogButtonType.Primary, + ), + dismissButtonProperties = WireDialogButtonProperties( + onClick = dialogState::dismiss, + text = stringResource(R.string.label_cancel), + type = WireDialogButtonType.Secondary, + ), + buttonsHorizontalAlignment = true + ) + } +} 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 a9ed4949afa..1055c721e54 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 @@ -42,7 +42,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -68,6 +67,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.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.calculateCurrentTab +import com.wire.android.ui.common.dialogs.ArchiveConversationDialog import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topBarElevation import com.wire.android.ui.common.topappbar.NavigationIconType @@ -221,7 +221,6 @@ fun GroupConversationDetailsScreen( } @OptIn( - ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class ) @@ -268,6 +267,7 @@ private fun GroupConversationDetailsContent( val deleteGroupDialogState = rememberVisibilityState() val leaveGroupDialogState = rememberVisibilityState() val clearConversationDialogState = rememberVisibilityState() + val archiveConversationDialogState = rememberVisibilityState() LaunchedEffect(conversationSheetState.conversationSheetContent) { // on each closing BottomSheet we revert BSContent to Home. @@ -282,6 +282,7 @@ private fun GroupConversationDetailsContent( deleteGroupDialogState.dismiss() leaveGroupDialogState.dismiss() clearConversationDialogState.dismiss() + archiveConversationDialogState.dismiss() } WireScaffold( topBar = { @@ -361,10 +362,12 @@ private fun GroupConversationDetailsContent( addConversationToFavourites = bottomSheetEventsHandler::onAddConversationToFavourites, moveConversationToFolder = bottomSheetEventsHandler::onMoveConversationToFolder, updateConversationArchiveStatus = { - conversationSheetContent?.let { + // Only show the confirmation dialog if the conversation is not archived + if (!it.isArchived) { + archiveConversationDialogState.show(it) + } else { bottomSheetEventsHandler.updateConversationArchiveStatus( - conversationId = it.conversationId, - shouldArchive = !it.isArchived, + dialogState = it, onMessage = closeBottomSheetAndShowSnackbarMessage ) } @@ -394,7 +397,14 @@ private fun GroupConversationDetailsContent( dialogState = clearConversationDialogState, isLoading = isLoading, onClearConversationContent = { - bottomSheetEventsHandler.onClearConversationContent(it, closeBottomSheetAndShowSnackbarMessage) + bottomSheetEventsHandler.onClearConversationContent(dialogState = it, onMessage = closeBottomSheetAndShowSnackbarMessage) + } + ) + + ArchiveConversationDialog( + dialogState = archiveConversationDialogState, + onArchiveButtonClicked = { + bottomSheetEventsHandler.updateConversationArchiveStatus(dialogState = it, onMessage = closeBottomSheetAndShowSnackbarMessage) } ) } 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 16fa680ab01..b257ace5767 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 @@ -380,15 +380,14 @@ class GroupConversationDetailsViewModel @Inject constructor( } override fun updateConversationArchiveStatus( - conversationId: ConversationId, - shouldArchive: Boolean, + dialogState: DialogState, timestamp: Long, onMessage: (UIText) -> Unit ) { viewModelScope.launch { + val shouldArchive = dialogState.isArchived.not() requestInProgress = true - val result = - withContext(dispatcher.io()) { updateConversationArchivedStatus(conversationId, shouldArchive, timestamp) } + val result = withContext(dispatcher.io()) { updateConversationArchivedStatus(conversationId, shouldArchive, timestamp) } requestInProgress = false when (result) { ArchiveStatusUpdateResult.Failure -> onMessage( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/menu/GroupConversationDetailsBottomSheetEventsHandler.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/menu/GroupConversationDetailsBottomSheetEventsHandler.kt index c81fcabb27e..7a3950fe786 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/menu/GroupConversationDetailsBottomSheetEventsHandler.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/menu/GroupConversationDetailsBottomSheetEventsHandler.kt @@ -32,8 +32,7 @@ interface GroupConversationDetailsBottomSheetEventsHandler { fun onAddConversationToFavourites(conversationId: ConversationId? = null) fun onMoveConversationToFolder(conversationId: ConversationId? = null) fun updateConversationArchiveStatus( - conversationId: ConversationId, - shouldArchive: Boolean, + dialogState: DialogState, timestamp: Long = DateTimeUtil.currentInstant().toEpochMilliseconds(), onMessage: (UIText) -> Unit ) @@ -53,8 +52,7 @@ interface GroupConversationDetailsBottomSheetEventsHandler { override fun onAddConversationToFavourites(conversationId: ConversationId?) {} override fun onMoveConversationToFolder(conversationId: ConversationId?) {} override fun updateConversationArchiveStatus( - conversationId: ConversationId, - shouldArchive: Boolean, + dialogState: DialogState, timestamp: Long, onMessage: (UIText) -> Unit ) { 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 f8ec9dcb98d..4b511ff7522 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 @@ -93,7 +93,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.scan import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.util.Date import javax.inject.Inject @@ -323,7 +322,7 @@ class ConversationListViewModel @Inject constructor( } fun blockUser(blockUserState: BlockUserDialogState) { - viewModelScope.launch(dispatcher.io()) { + viewModelScope.launch { requestInProgress = true val state = when (val result = blockUserUseCase(blockUserState.userId)) { BlockUserResult.Success -> { @@ -342,7 +341,7 @@ class ConversationListViewModel @Inject constructor( } fun unblockUser(userId: UserId) { - viewModelScope.launch(dispatcher.io()) { + viewModelScope.launch { requestInProgress = true when (val result = unblockUserUseCase(userId)) { UnblockUserResult.Success -> { @@ -362,11 +361,7 @@ class ConversationListViewModel @Inject constructor( fun leaveGroup(leaveGroupState: GroupDialogState) { viewModelScope.launch { requestInProgress = true - val response = withContext(dispatcher.io()) { - leaveConversation( - leaveGroupState.conversationId - ) - } + val response = leaveConversation(leaveGroupState.conversationId) when (response) { is RemoveMemberFromConversationUseCase.Result.Failure -> homeSnackBarState.emit(HomeSnackbarState.LeaveConversationError) @@ -382,7 +377,7 @@ class ConversationListViewModel @Inject constructor( fun deleteGroup(groupDialogState: GroupDialogState) { viewModelScope.launch { requestInProgress = true - when (withContext(dispatcher.io()) { deleteTeamConversation(groupDialogState.conversationId) }) { + when (deleteTeamConversation(groupDialogState.conversationId)) { is Result.Failure.GenericFailure -> homeSnackBarState.emit(HomeSnackbarState.DeleteConversationGroupError) Result.Failure.NoTeamFailure -> homeSnackBarState.emit(HomeSnackbarState.DeleteConversationGroupError) Result.Success -> homeSnackBarState.emit( @@ -415,32 +410,30 @@ class ConversationListViewModel @Inject constructor( fun moveConversationToFolder(id: String = "") { } - fun moveConversationToArchive( - conversationId: ConversationId, - isArchiving: Boolean, - timestamp: Long = DateTimeUtil.currentInstant().toEpochMilliseconds() - ) { - viewModelScope.launch { - requestInProgress = true - val result = withContext(dispatcher.io()) { updateConversationArchivedStatus(conversationId, isArchiving, timestamp) } - requestInProgress = false - when (result) { - is ArchiveStatusUpdateResult.Failure -> { - homeSnackBarState.emit(HomeSnackbarState.UpdateArchivingStatusError(isArchiving)) - } + fun moveConversationToArchive(dialogState: DialogState, timestamp: Long = DateTimeUtil.currentInstant().toEpochMilliseconds()) = + with(dialogState) { + viewModelScope.launch { + val isArchiving = !isArchived + requestInProgress = true + val result = updateConversationArchivedStatus(conversationId, isArchiving, timestamp) + requestInProgress = false + when (result) { + is ArchiveStatusUpdateResult.Failure -> { + homeSnackBarState.emit(HomeSnackbarState.UpdateArchivingStatusError(isArchiving)) + } - is ArchiveStatusUpdateResult.Success -> { - homeSnackBarState.emit(HomeSnackbarState.UpdateArchivingStatusSuccess(isArchiving)) + is ArchiveStatusUpdateResult.Success -> { + homeSnackBarState.emit(HomeSnackbarState.UpdateArchivingStatusSuccess(isArchiving)) + } } } } - } fun clearConversationContent(dialogState: DialogState) { viewModelScope.launch { requestInProgress = true with(dialogState) { - val result = withContext(dispatcher.io()) { clearConversationContentUseCase(conversationId) } + val result = clearConversationContentUseCase(conversationId) requestInProgress = false clearContentSnackbarResult(result, conversationTypeDetail) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt index 7709e7f7b17..49fa82ab500 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt @@ -36,6 +36,7 @@ import com.wire.android.ui.calling.common.MicrophoneBTPermissionsDeniedDialog import com.wire.android.ui.common.bottomsheet.conversation.ConversationOptionNavigation import com.wire.android.ui.common.bottomsheet.conversation.ConversationSheetContent import com.wire.android.ui.common.bottomsheet.conversation.rememberConversationSheetState +import com.wire.android.ui.common.dialogs.ArchiveConversationDialog import com.wire.android.ui.common.dialogs.BlockUserDialogContent import com.wire.android.ui.common.dialogs.BlockUserDialogState import com.wire.android.ui.common.dialogs.UnblockUserDialogContent @@ -155,13 +156,7 @@ fun ConversationRouterHomeBridge( }, addConversationToFavourites = viewModel::addConversationToFavourites, moveConversationToFolder = viewModel::moveConversationToFolder, - updateConversationArchiveStatus = { - viewModel.moveConversationToArchive( - conversationId = it.conversationId, - isArchiving = !it.isArchived - ) - onCloseBottomSheet() - }, + updateConversationArchiveStatus = archiveConversationDialogState::show, clearConversationContent = clearContentDialogState::show, blockUser = blockUserDialogState::show, unblockUser = unblockUserDialogState::show, @@ -281,6 +276,11 @@ fun ConversationRouterHomeBridge( onClearConversationContent = viewModel::clearConversationContent ) + ArchiveConversationDialog( + dialogState = archiveConversationDialogState, + onArchiveButtonClicked = viewModel::moveConversationToArchive + ) + BackHandler(conversationItemType == ConversationItemType.SEARCH) { closeSearch() } @@ -295,6 +295,7 @@ class ConversationRouterState( val blockUserDialogState: VisibilityState, val unblockUserDialogState: VisibilityState, val clearContentDialogState: VisibilityState, + val archiveConversationDialogState: VisibilityState, requestInProgress: Boolean ) { @@ -326,6 +327,7 @@ fun rememberConversationRouterState( val blockUserDialogState = rememberVisibilityState() val unblockUserDialogState = rememberVisibilityState() val clearContentDialogState = rememberVisibilityState() + val archiveConversationDialogState = rememberVisibilityState() LaunchedEffect(Unit) { homeSnackBarState.collect { onSnackBarStateChanged(it) } @@ -343,7 +345,8 @@ fun rememberConversationRouterState( blockUserDialogState, unblockUserDialogState, clearContentDialogState, - requestInProgress + archiveConversationDialogState, + requestInProgress, ) } @@ -354,6 +357,7 @@ fun rememberConversationRouterState( blockUserDialogState.dismiss() unblockUserDialogState.dismiss() clearContentDialogState.dismiss() + archiveConversationDialogState.dismiss() } conversationRouterState.requestInProgress = requestInProgress 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 3ab61983037..db7eab12d80 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 @@ -181,6 +181,7 @@ fun OtherProfileScreenContent( val unblockUserDialogState = rememberVisibilityState() val removeMemberDialogState = rememberVisibilityState() val clearConversationDialogState = rememberVisibilityState() + val archivingConversationDialogState = rememberVisibilityState() val getBottomSheetVisibility: () -> Boolean = remember(sheetState) { { sheetState.isVisible } } val bottomSheetState = remember { OtherUserBottomSheetState() } bottomSheetState.setContents(state.conversationSheetContent, state.groupState) @@ -234,6 +235,7 @@ fun OtherProfileScreenContent( unblockUserDialogState.dismiss() removeMemberDialogState.dismiss() clearConversationDialogState.dismiss() + archivingConversationDialogState.dismiss() } CollapsingTopBarScaffold( @@ -283,6 +285,7 @@ fun OtherProfileScreenContent( blockUser = blockUserDialogState::show, unblockUser = unblockUserDialogState::show, clearContent = clearConversationDialogState::show, + archivingStatusState = archivingConversationDialogState::show, closeBottomSheet = closeBottomSheet, ) } 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 f1f2b94f81d..7c5725b1020 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 @@ -33,6 +33,7 @@ fun OtherUserProfileBottomSheetContent( bottomSheetState: OtherUserBottomSheetState, eventsHandler: OtherUserProfileBottomSheetEventsHandler, clearContent: (DialogState) -> Unit, + archivingStatusState: (DialogState) -> Unit, blockUser: (BlockUserDialogState) -> Unit, unblockUser: (UnblockUserDialogState) -> Unit, closeBottomSheet: () -> Unit, @@ -54,10 +55,11 @@ fun OtherUserProfileBottomSheetContent( addConversationToFavourites = eventsHandler::onAddConversationToFavourites, moveConversationToFolder = eventsHandler::onMoveConversationToFolder, updateConversationArchiveStatus = { - eventsHandler.onMoveConversationToArchive( - conversationId = it.conversationId, - isArchivingConversation = !it.isArchived - ) + if (!it.isArchived) { + archivingStatusState(it) + } else { + eventsHandler.onMoveConversationToArchive(it.conversationId, isArchivingConversation = false) + } }, clearConversationContent = clearContent, blockUser = blockUser, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 275fdfa3e8c..df9a75d4d3c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -726,10 +726,14 @@ User could not be unblocked There was an error while deleting conversation You can only send up to 20 files at once + Conversation was archived Conversation was unarchived Conversation could not be archived Conversation could not be unarchived + Archive conversation? + The conversation will be moved to the archive section. You can still participate in the conversation and receive new activity, but the group will be muted. You can un-archive the conversation any time. + Archive MessageComposeInputState transition HorizontalBouncingWritingPen transition 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 29bec253ee8..cdfb6dcc203 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 @@ -31,6 +31,7 @@ import com.wire.android.ui.common.bottomsheet.conversation.ConversationTypeDetai import com.wire.android.ui.home.conversations.details.participants.GroupConversationAllParticipantsNavArgs import com.wire.android.ui.home.conversations.details.participants.model.ConversationParticipantsData import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase +import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.navArgs import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails @@ -138,8 +139,17 @@ class GroupConversationDetailsViewModelTest { participants = members.take(GroupConversationDetailsViewModel.MAX_NUMBER_OF_PARTICIPANTS), allParticipantsCount = members.size ) - val conversationDetails = testGroup.copy(conversation = testGroup.conversation.copy(name = "Group name 1")) + val dialogState = DialogState( + conversationId = conversationDetails.conversation.id, + conversationName = conversationDetails.conversation.name.orEmpty(), + conversationTypeDetail = ConversationTypeDetail.Group( + conversationId = conversationDetails.conversation.id, + isCreator = conversationDetails.isSelfUserCreator + ), + isArchived = conversationDetails.conversation.archived + ) + val (arrangement, viewModel) = GroupConversationDetailsViewModelArrangement() .withConversationDetailUpdate(conversationDetails) .withConversationMembersUpdate(conversationParticipantsData) @@ -148,16 +158,15 @@ class GroupConversationDetailsViewModelTest { // When viewModel.updateConversationArchiveStatus( - conversationId = viewModel.conversationId, - shouldArchive = true, + dialogState = dialogState, timestamp = archivingEventTimestamp - ) {} + ) { } // Then coVerify(exactly = 1) { arrangement.updateConversationArchivedStatus( conversationId = viewModel.conversationId, - shouldArchiveConversation = true, + shouldArchiveConversation = !conversationDetails.conversation.archived, archivedStatusTimestamp = archivingEventTimestamp ) } @@ -177,7 +186,17 @@ class GroupConversationDetailsViewModelTest { allParticipantsCount = members.size ) - val conversationDetails = testGroup.copy(conversation = testGroup.conversation.copy(name = "Group name 1")) + val conversationDetails = testGroup.copy(conversation = testGroup.conversation.copy(name = "Group name 1", archived = true)) + val dialogState = DialogState( + conversationId = conversationDetails.conversation.id, + conversationName = conversationDetails.conversation.name.orEmpty(), + conversationTypeDetail = ConversationTypeDetail.Group( + conversationId = conversationDetails.conversation.id, + isCreator = conversationDetails.isSelfUserCreator + ), + isArchived = conversationDetails.conversation.archived + ) + val (arrangement, viewModel) = GroupConversationDetailsViewModelArrangement() .withConversationDetailUpdate(conversationDetails) .withConversationMembersUpdate(conversationParticipantsData) @@ -185,7 +204,10 @@ class GroupConversationDetailsViewModelTest { .arrange() // When - viewModel.updateConversationArchiveStatus(viewModel.conversationId, false, archivingEventTimestamp) {} + viewModel.updateConversationArchiveStatus( + dialogState = dialogState, + timestamp = archivingEventTimestamp + ) {} // Then coVerify(exactly = 1) { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt index 6bf139efa3a..336156ca679 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt @@ -28,6 +28,7 @@ import com.wire.android.config.mockUri import com.wire.android.framework.TestConversationDetails import com.wire.android.mapper.UserTypeMapper import com.wire.android.model.UserAvatarData +import com.wire.android.ui.common.bottomsheet.conversation.ConversationTypeDetail import com.wire.android.ui.common.dialogs.BlockUserDialogState import com.wire.android.ui.home.HomeSnackbarState import com.wire.android.ui.home.conversations.model.UILastMessageContent @@ -37,6 +38,7 @@ import com.wire.android.ui.home.conversationslist.model.ConversationFolder import com.wire.android.ui.home.conversationslist.model.ConversationInfo import com.wire.android.ui.home.conversationslist.model.ConversationItem import com.wire.android.ui.home.conversationslist.model.ConversationsSource +import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.util.orDefault import com.wire.android.util.ui.WireSessionImageLoader @@ -331,27 +333,53 @@ class ConversationListViewModelTest { @Test fun `given a valid conversation state, when archiving it correctly, then the right success message is shown`() = runTest { val isArchiving = true + val dialogState = DialogState( + conversationItem.conversationId, + conversationItem.conversationInfo.name, + ConversationTypeDetail.Private(null, conversationItem.userId, BlockingState.NOT_BLOCKED), + !isArchiving + ) val archivingTimestamp = 123456789L coEvery { updateConversationArchivedStatus(any(), any(), any()) } returns ArchiveStatusUpdateResult.Success conversationListViewModel.homeSnackBarState.test { - conversationListViewModel.moveConversationToArchive(conversationId, isArchiving, archivingTimestamp) + conversationListViewModel.moveConversationToArchive(dialogState, archivingTimestamp) expectMostRecentItem() shouldBeEqualTo HomeSnackbarState.UpdateArchivingStatusSuccess(isArchiving = isArchiving) } + coVerify(exactly = 1) { + updateConversationArchivedStatus.invoke( + dialogState.conversationId, + !dialogState.isArchived, + archivingTimestamp + ) + } } @Test - fun `given a valid conversation state, when archiving it with an error, then the right failure message is shown`() = runTest { - val isArchiving = true + fun `given a valid conversation state, when un-archiving it with an error, then the right failure message is shown`() = runTest { + val isArchiving = false + val dialogState = DialogState( + conversationItem.conversationId, + conversationItem.conversationInfo.name, + ConversationTypeDetail.Private(null, conversationItem.userId, BlockingState.NOT_BLOCKED), + !isArchiving + ) val archivingTimestamp = 123456789L coEvery { updateConversationArchivedStatus(any(), any(), any()) } returns ArchiveStatusUpdateResult.Failure conversationListViewModel.homeSnackBarState.test { - conversationListViewModel.moveConversationToArchive(conversationId, isArchiving, archivingTimestamp) + conversationListViewModel.moveConversationToArchive(dialogState, archivingTimestamp) expectMostRecentItem() shouldBeEqualTo HomeSnackbarState.UpdateArchivingStatusError(isArchiving = isArchiving) } + coVerify(exactly = 1) { + updateConversationArchivedStatus.invoke( + dialogState.conversationId, + !dialogState.isArchived, + archivingTimestamp + ) + } } companion object { @@ -363,7 +391,7 @@ class ConversationListViewModelTest { private val conversationItem = ConversationItem.PrivateConversation( userAvatarData = UserAvatarData(), conversationInfo = ConversationInfo( - name = "", + name = "Some dummy name", membership = Membership.None ), conversationId = conversationId, diff --git a/kalium b/kalium index 751cd750a5e..1f49504c634 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 751cd750a5e0430a9556369336a5de1168065d9b +Subproject commit 1f49504c6341c017789630fc85c783390d8c1aa2 From 25532475bd601d2871c78155878d1591b9962b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Thu, 5 Oct 2023 12:13:13 +0200 Subject: [PATCH 4/4] feat: cherry pick improvements (#2298) --- .../workflows/cherry-pick-rc-to-develop.yml | 112 +++++++++++++++--- 1 file changed, 93 insertions(+), 19 deletions(-) diff --git a/.github/workflows/cherry-pick-rc-to-develop.yml b/.github/workflows/cherry-pick-rc-to-develop.yml index 9d517279796..94e5db55023 100644 --- a/.github/workflows/cherry-pick-rc-to-develop.yml +++ b/.github/workflows/cherry-pick-rc-to-develop.yml @@ -1,3 +1,25 @@ +# GitHub Action: Cherry-pick from `release/candidate` to `TARGET_BRANCH` +# +# This action automates the process of cherry-picking merged PRs from `release/candidate` branch to `TARGET_BRANCH`. +# It is triggered whenever a pull request is merged into `release/candidate`. +# +# The action performs the following steps: +# 1. Checkout the merged PR. +# 2. If changes are made outside the specified submodule or no submodule is specified, the action proceeds. +# 3. If a submodule name is provided in the `SUBMODULE_NAME` environment variable: +# a. The action creates a temporary branch. +# b. Updates the submodule to its latest version from `develop`. +# c. Commits the submodule updates. +# 4. Squashes the commit with the commit message of the merged PR (if a submodule was updated). +# 5. Cherry-picks the squashed (or original if no squashing occurred) commit to a new branch based on `develop`. +# 6. If any conflicts arise during the cherry-pick, they are committed. +# 7. The branch with the cherry-picked changes is pushed. +# 8. A new pull request is created against `develop` with the cherry-picked changes. +# +# Note: Ensure you add a "cherry-pick" label to your project. This label is required for the creation of cherry-picked PRs. +# If needed, you can also set the `TARGET_BRANCH` environment variable to specify a different target branch for the cherry-pick. +# By default, it's set to `develop`. + name: "Cherry-pick from rc to develop" on: @@ -7,6 +29,11 @@ on: types: - closed +env: + + TARGET_BRANCH: develop + SUBMODULE_NAME: kalium + jobs: cherry-pick: runs-on: ubuntu-latest @@ -20,6 +47,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + submodules: recursive - name: Append -cherry-pick to branch name id: extract @@ -29,40 +57,86 @@ jobs: echo "New branch name: $NEW_BRANCH_NAME" echo "newBranchName=$NEW_BRANCH_NAME" >> $GITHUB_ENV - - name: Check if changes only in kalium submodule + - name: Check for changes excluding submodule id: check_changes run: | - NUM_CHANGES=$(git diff origin/develop --name-only | grep -v '^kalium/' | wc -l) + if [[ -n "${{ env.SUBMODULE_NAME }}" ]]; then + # If SUBMODULE_NAME is set + NUM_CHANGES=$(git diff origin/${{ env.TARGET_BRANCH }} --name-only | grep -v "^${{ env.SUBMODULE_NAME }}/" | wc -l) + else + # If SUBMODULE_NAME is not set + NUM_CHANGES=$(git diff origin/${{ env.TARGET_BRANCH }} --name-only | wc -l) + fi if [ "$NUM_CHANGES" -gt 0 ]; then - echo "::set-output name=shouldCherryPick::true" + echo "shouldCherryPick=true" >> $GITHUB_ENV else - echo "No changes outside of kalium submodule, skipping cherry-pick" - echo "::set-output name=shouldCherryPick::false" + if [[ -n "${{ env.SUBMODULE_NAME }}" ]]; then + echo "No changes outside of ${{ env.SUBMODULE_NAME }} submodule, skipping cherry-pick" + else + echo "No changes detected, skipping cherry-pick" + fi + echo "shouldCherryPick=false" >> $GITHUB_ENV fi - uses: fregante/setup-git-user@v2 + - name: Update submodule + if: env.SUBMODULE_NAME && env.shouldCherryPick == 'true' + run: | + set -x + # Create a temporary branch and get the last commit message + git checkout -b temp-branch-for-cherry-pick + LAST_COMMIT_MESSAGE=$(git log --format=%B -n 1 ${{ github.event.pull_request.merge_commit_sha }}) + cd ${{ env.SUBMODULE_NAME }} + git checkout ${{ env.TARGET_BRANCH }} + git pull origin ${{ env.TARGET_BRANCH }} + cd .. + git add ${{ env.SUBMODULE_NAME }} + git commit -m "Update submodule ${{ env.SUBMODULE_NAME }} to latest from ${{ env.TARGET_BRANCH }}" + echo "lastCommitMessage=LAST_COMMIT_MESSAGE" >> $GITHUB_ENV + + - name: Get Cherry-pick commit + id: get-cherry + if: env.shouldCherryPick == 'true' + run: | + if [[ -n "${{ env.SUBMODULE_NAME }}" ]]; then + # If SUBMODULE_NAME is set + git reset --soft HEAD~2 + git commit -m "${{ env.lastCommitMessage }}" + fi + + # Get the SHA of the current commit (either squashed or not based on the condition above) + CHERRY_PICK_COMMIT=$(git rev-parse HEAD) + echo "cherryPickCommit=$CHERRY_PICK_COMMIT" >> $GITHUB_ENV + - name: Cherry-pick commits - id: cherry - if: steps.check_changes.outputs.shouldCherryPick == 'true' + id: commit-cherry-pick + if: env.shouldCherryPick == 'true' run: | - git fetch origin develop:develop - git checkout -b ${{ env.newBranchName }} develop - # Cherry-picking the last commit on the base branch - OUTPUT=$(git cherry-pick ${{ github.event.pull_request.merge_commit_sha }} --strategy-option theirs || true) - CONFLICTS=$(echo "$OUTPUT" | grep 'CONFLICT' || echo "") - if [ -n "$CONFLICTS" ]; then - git add . - git cherry-pick --continue || true + # Checkout the desired branch and cherry-pick the commit + git checkout ${{ env.TARGET_BRANCH }} + git checkout -b ${{ env.newBranchName }} + OUTPUT=$(git cherry-pick ${{ env.cherryPickCommit }} || true) + + # Handle conflicts + CONFLICTED_FILES=$(git diff --name-only --diff-filter=U) + if [[ "$OUTPUT" == *"CONFLICT"* ]]; then + # Commit the remaining conflicts + git commit -am "Commit with unresolved merge conflicts outside of ${{ env.SUBMODULE_NAME }}" fi + + # Push branch and remove temp git push origin ${{ env.newBranchName }} || (echo "Failed to push to origin" && exit 1) - echo "conflicts=$CONFLICTS" >> $GITHUB_ENV + echo "conflictedFiles=$CONFLICTED_FILES" >> $GITHUB_ENV + if [[ -n "${{ env.SUBMODULE_NAME }}" ]]; then + git branch -D temp-branch-for-cherry-pick + fi - name: Create PR - if: steps.check_changes.outputs.shouldCherryPick == 'true' + if: env.shouldCherryPick == 'true' env: PR_TITLE: ${{ github.event.pull_request.title }} PR_BRANCH: ${{ env.newBranchName }} PR_ASSIGNEE: ${{ github.event.pull_request.user.login }} - PR_BODY: "${{ format('Cherry pick from the original PR: \n- #{0}\n\n---- \n\n ⚠️ Conflicts during cherry-pick:\n{1}\n\n{2}', github.event.pull_request.number, env.conflicts, github.event.pull_request.body) }}" - run: gh pr create --title "$PR_TITLE" --body "$PR_BODY" --base develop --head "$PR_BRANCH" --label "cherry-pick" --assignee "$PR_ASSIGNEE" + PR_BODY: "${{ format('Cherry pick from the original PR: \n- #{0}\n\n---- \n\n ⚠️ Conflicts during cherry-pick:\n{1}\n\n{2}', github.event.pull_request.number, env.conflictedFiles, github.event.pull_request.body) }}" + run: gh pr create --title "$PR_TITLE" --body "$PR_BODY" --base ${{ env.TARGET_BRANCH }} --head "$PR_BRANCH" --label "cherry-pick" --assignee "$PR_ASSIGNEE"