diff --git a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/UserNotFoundDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/UserNotFoundDialog.kt new file mode 100644 index 00000000000..95d5bd774a8 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/UserNotFoundDialog.kt @@ -0,0 +1,74 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.android.ui.common.dialogs + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.window.DialogProperties +import com.wire.android.R +import com.wire.android.ui.common.WireDialog +import com.wire.android.ui.common.WireDialogButtonProperties +import com.wire.android.ui.common.WireDialogButtonType +import com.wire.android.ui.common.wireDialogPropertiesBuilder +import com.wire.android.ui.theme.WireTheme +import com.wire.android.util.ui.PreviewMultipleThemes + +@Composable +fun UserNotFoundDialog( + onActionButtonClicked: () -> Unit +) { + UserNotFoundDialogContent( + onConfirm = onActionButtonClicked, + onDismiss = onActionButtonClicked, + buttonText = R.string.label_ok, + dialogProperties = wireDialogPropertiesBuilder( + dismissOnBackPress = false, + dismissOnClickOutside = false + ) + ) +} + +@Composable +fun UserNotFoundDialogContent( + @StringRes buttonText: Int, + onConfirm: () -> Unit, + dialogProperties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false), + onDismiss: () -> Unit +) { + WireDialog( + title = stringResource(R.string.connection_label_user_not_found_warning_title), + text = stringResource(R.string.connection_label_user_not_found_warning_description), + onDismiss = onDismiss, + optionButton1Properties = WireDialogButtonProperties( + text = stringResource(buttonText), + onClick = onConfirm, + type = WireDialogButtonType.Primary + ), + properties = dialogProperties + ) +} + +@PreviewMultipleThemes +@Composable +fun PreviewUserNotFoundDialog() { + WireTheme { + UserNotFoundDialogContent(onConfirm = { }, onDismiss = { }, buttonText = R.string.label_ok) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllPeopleScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllPeopleScreen.kt index a1f22068e10..7f4d4676784 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllPeopleScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllPeopleScreen.kt @@ -25,7 +25,6 @@ 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.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn @@ -37,16 +36,15 @@ import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import com.wire.android.R import com.wire.android.model.Clickable import com.wire.android.model.ItemActionType import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dimensions -import com.wire.android.ui.common.progress.WireCircularProgressIndicator +import com.wire.android.ui.common.progress.CenteredCircularProgressBarIndicator +import com.wire.android.ui.home.conversations.search.widget.SearchFailureBox import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.home.newconversation.model.Contact import com.wire.android.ui.theme.WireTheme @@ -82,10 +80,15 @@ fun SearchAllPeopleScreen( onPublicResultsExpansionChanged: (Boolean) -> Unit = {}, lazyListState: LazyListState = rememberLazyListState() ) { - if (contactsSearchResult.isEmpty() && publicSearchResult.isEmpty()) { - EmptySearchQueryScreen() - } else { - SearchResult( + val emptyResults = contactsSearchResult.isEmpty() && publicSearchResult.isEmpty() + when { + isLoading -> CenteredCircularProgressBarIndicator() + + searchQuery.isBlank() && emptyResults -> EmptySearchQueryScreen() + + searchQuery.isNotBlank() && emptyResults -> SearchFailureBox(R.string.label_no_results_found) + + else -> SearchResult( searchQuery = searchQuery, publicSearchResult = publicSearchResult, contactsSearchResult = contactsSearchResult, @@ -94,7 +97,6 @@ fun SearchAllPeopleScreen( onOpenUserProfile = onOpenUserProfile, lazyListState = lazyListState, isSearchActive = isSearchActive, - isLoading = isLoading, actionType = actionType, selectedContactResultsExpanded = selectedContactResultsExpanded, onSelectedContactResultsExpansionChanged = onSelectedContactResultsExpansionChanged, @@ -112,7 +114,6 @@ private fun SearchResult( contactsSearchResult: ImmutableList, publicSearchResult: ImmutableList, contactsSelectedSearchResult: ImmutableList, - isLoading: Boolean, isSearchActive: Boolean, actionType: ItemActionType, onChecked: (Boolean, Contact) -> Unit, @@ -140,8 +141,7 @@ private fun SearchResult( searchTitle = context.getString(R.string.label_selected) + " (${contactsSelectedSearchResult.size})", searchQuery = searchQuery, onChecked = onChecked, - isLoading = isLoading, - contactSearchResult = contactsSelectedSearchResult.map { it to true }.toImmutableList(), + searchResult = contactsSelectedSearchResult.map { it to true }.toImmutableList(), allItemsVisible = true, // for selected contacts we always show all items showMoreOrLessButtonVisible = false, onShowAllButtonClicked = searchPeopleScreenState::toggleShowAllContactsResult, @@ -157,8 +157,7 @@ private fun SearchResult( searchTitle = context.getString(R.string.label_contacts), searchQuery = searchQuery, onChecked = onChecked, - isLoading = isLoading, - contactSearchResult = contactsSearchResult.map { it to false }.toImmutableList(), + searchResult = contactsSearchResult.map { it to false }.toImmutableList(), allItemsVisible = !isSearchActive || searchPeopleScreenState.contactsAllResultsCollapsed, showMoreOrLessButtonVisible = isSearchActive, onShowAllButtonClicked = searchPeopleScreenState::toggleShowAllContactsResult, @@ -173,8 +172,7 @@ private fun SearchResult( externalSearchResults( searchTitle = context.getString(R.string.label_public_wire), searchQuery = searchQuery, - contactSearchResult = publicSearchResult, - isLoading = isLoading, + searchResult = publicSearchResult, allItemsVisible = searchPeopleScreenState.publicResultsCollapsed, showMoreOrLessButtonVisible = isSearchActive, onShowAllButtonClicked = searchPeopleScreenState::toggleShowAllPublicResult, @@ -193,78 +191,6 @@ private fun SearchResult( @Suppress("LongParameterList") private fun LazyListScope.internalSearchResults( - searchTitle: String, - searchQuery: String, - onChecked: (Boolean, Contact) -> Unit, - actionType: ItemActionType, - isLoading: Boolean, - contactSearchResult: ImmutableList>, - allItemsVisible: Boolean, - showMoreOrLessButtonVisible: Boolean, - onShowAllButtonClicked: () -> Unit, - onOpenUserProfile: (Contact) -> Unit, - expanded: Boolean, - onExpansionChanged: (Boolean) -> Unit, -) { - when { - isLoading -> { - inProgressItem() - } - - else -> { - internalSuccessItem( - searchTitle = searchTitle, - allItemsVisible = allItemsVisible, - showMoreOrLessButtonVisible = showMoreOrLessButtonVisible, - onChecked = onChecked, - searchResult = contactSearchResult, - searchQuery = searchQuery, - onShowAllButtonClicked = onShowAllButtonClicked, - onOpenUserProfile = onOpenUserProfile, - actionType = actionType, - expanded = expanded, - onExpansionChanged = onExpansionChanged, - ) - } - } -} - -@Suppress("LongParameterList") -private fun LazyListScope.externalSearchResults( - searchTitle: String, - searchQuery: String, - contactSearchResult: ImmutableList, - isLoading: Boolean, - allItemsVisible: Boolean, - showMoreOrLessButtonVisible: Boolean, - onShowAllButtonClicked: () -> Unit, - onOpenUserProfile: (Contact) -> Unit, - expanded: Boolean, - onExpansionChanged: (Boolean) -> Unit, -) { - when { - isLoading -> { - inProgressItem() - } - - else -> { - externalSuccessItem( - searchTitle = searchTitle, - allItemsVisible = allItemsVisible, - showMoreOrLessButtonVisible = showMoreOrLessButtonVisible, - searchResult = contactSearchResult, - searchQuery = searchQuery, - onShowAllButtonClicked = onShowAllButtonClicked, - onOpenUserProfile = onOpenUserProfile, - expanded = expanded, - onExpansionChanged = onExpansionChanged, - ) - } - } -} - -@Suppress("LongParameterList") -private fun LazyListScope.internalSuccessItem( searchTitle: String, allItemsVisible: Boolean, showMoreOrLessButtonVisible: Boolean, @@ -330,7 +256,7 @@ private fun LazyListScope.internalSuccessItem( } @Suppress("LongParameterList") -private fun LazyListScope.externalSuccessItem( +private fun LazyListScope.externalSearchResults( searchTitle: String, allItemsVisible: Boolean, showMoreOrLessButtonVisible: Boolean, @@ -383,23 +309,6 @@ private fun LazyListScope.externalSuccessItem( } } -fun LazyListScope.inProgressItem() { - item { - Box( - Modifier - .fillMaxWidth() - .height(224.dp) - ) { - WireCircularProgressIndicator( - progressColor = Color.Black, - modifier = Modifier.align( - Alignment.Center - ) - ) - } - } -} - @Composable private fun ShowButton( isShownAll: Boolean, @@ -445,10 +354,11 @@ fun PreviewSearchAllPeopleScreen_Loading() = WireTheme { @PreviewMultipleThemes @Composable -fun PreviewSearchAllPeopleScreen_EmptyList() = WireTheme { +fun PreviewSearchAllPeopleScreen_InitialResults() = WireTheme { + val contacts = previewContactsList(count = 10, startIndex = 0, isContact = true) SearchAllPeopleScreen( - searchQuery = "Search query", - contactsSearchResult = persistentListOf(), + searchQuery = "", + contactsSearchResult = contacts, publicSearchResult = persistentListOf(), contactsSelectedSearchResult = persistentListOf(), isLoading = false, @@ -461,11 +371,10 @@ fun PreviewSearchAllPeopleScreen_EmptyList() = WireTheme { @PreviewMultipleThemes @Composable -fun PreviewSearchAllPeopleScreen_NoSearch() = WireTheme { - val contacts = previewContactsList(count = 10, startIndex = 0, isContact = true) +fun PreviewSearchAllPeopleScreen_EmptyInitialResults() = WireTheme { SearchAllPeopleScreen( searchQuery = "", - contactsSearchResult = contacts, + contactsSearchResult = persistentListOf(), publicSearchResult = persistentListOf(), contactsSelectedSearchResult = persistentListOf(), isLoading = false, @@ -514,6 +423,23 @@ fun PreviewSearchAllPeopleScreen_SearchResults_TypeCheck() = WireTheme { ) } +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllPeopleScreen_EmptySearchResults() = WireTheme { + SearchAllPeopleScreen( + searchQuery = "Con", + contactsSearchResult = persistentListOf(), + publicSearchResult = persistentListOf(), + contactsSelectedSearchResult = persistentListOf(), + isLoading = false, + isSearchActive = true, + actionType = ItemActionType.CLICK, + onChecked = { _, _ -> }, + onOpenUserProfile = {}, + selectedContactResultsExpanded = true, + ) +} + private fun previewContact(index: Int, isContact: Boolean) = Contact( id = index.toString(), domain = "wire.com", diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllServicesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllServicesScreen.kt index a9d0a1aafec..1c7221ac7aa 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllServicesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllServicesScreen.kt @@ -39,9 +39,15 @@ import com.wire.android.ui.common.UserProfileAvatar import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.progress.CenteredCircularProgressBarIndicator import com.wire.android.ui.home.conversations.search.widget.SearchFailureBox +import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.home.newconversation.model.Contact import com.wire.android.util.extension.folderWithElements +import com.wire.android.ui.theme.WireTheme +import com.wire.android.util.ui.PreviewMultipleThemes +import com.wire.kalium.logic.data.user.ConnectionState import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList @Composable fun SearchAllServicesScreen( @@ -59,7 +65,6 @@ fun SearchAllServicesScreen( onServiceClicked = onServiceClicked, result = searchServicesViewModel.state.result, lazyListState = lazyListState, - error = searchServicesViewModel.state.error, isLoading = searchServicesViewModel.state.isLoading ) } @@ -69,27 +74,23 @@ private fun SearchAllServicesContent( searchQuery: String, result: ImmutableList, isLoading: Boolean, - error: Boolean, onServiceClicked: (Contact) -> Unit, lazyListState: LazyListState = rememberLazyListState() ) { when { isLoading -> CenteredCircularProgressBarIndicator() - error -> SearchFailureBox(failureMessage = R.string.label_general_error) // TODO(user experience): what to do when user team has no services? - result.isEmpty() -> { - EmptySearchQueryScreen() - } + searchQuery.isBlank() && result.isEmpty() -> EmptySearchQueryScreen() - else -> { - SuccessServicesList( - searchQuery = searchQuery, - onServiceClicked = onServiceClicked, - services = result, - lazyListState = lazyListState - ) - } + searchQuery.isNotBlank() && result.isEmpty() -> SearchFailureBox(R.string.label_no_results_found) + + else -> SuccessServicesList( + searchQuery = searchQuery, + onServiceClicked = onServiceClicked, + services = result, + lazyListState = lazyListState + ) } } @@ -141,3 +142,46 @@ private fun SuccessServicesList( } } } + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllServicesScreen_Loading() = WireTheme { + SearchAllServicesContent("", persistentListOf(), true, {}) +} + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllServicesScreen_InitialResults() = WireTheme { + SearchAllServicesContent("", previewServiceList(count = 10).toPersistentList(), false, {}) +} + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllServicesScreen_EmptyInitialResults() = WireTheme { + SearchAllServicesContent("", persistentListOf(), false, {}) +} + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllServicesScreen_SearchResults() = WireTheme { + SearchAllServicesContent("Serv", previewServiceList(count = 10).toPersistentList(), false, {}) +} + +@PreviewMultipleThemes +@Composable +fun PreviewSearchAllServicesScreen_EmptySearchResults() = WireTheme { + SearchAllServicesContent("Serv", persistentListOf(), false, {}) +} + +private fun previewService(index: Int) = Contact( + id = index.toString(), + domain = "wire.com", + name = "Service nr $index", + handle = "service_$index", + connectionState = ConnectionState.NOT_CONNECTED, + membership = Membership.Service, +) + +private fun previewServiceList(count: Int): List = buildList { + repeat(count) { index -> add(previewService(index)) } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchServicesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchServicesViewModel.kt index 31aef8992f5..16b345f61ad 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchServicesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchServicesViewModel.kt @@ -46,7 +46,7 @@ class SearchServicesViewModel @Inject constructor( private val searchServicesByName: SearchServicesByNameUseCase, ) : ViewModel() { private val searchQueryTextFlow = MutableStateFlow(String.EMPTY) - var state: SearchServicesState by mutableStateOf(SearchServicesState()) + var state: SearchServicesState by mutableStateOf(SearchServicesState(isLoading = true)) private set init { @@ -68,15 +68,12 @@ class SearchServicesViewModel @Inject constructor( private fun search(query: String) { viewModelScope.launch { - if (query.isEmpty()) { - getAllServices().first().also { services -> - state = state.copy(result = services.map(contactMapper::fromService).toImmutableList(), searchQuery = query) - } + val result = if (query.isEmpty()) { + getAllServices().first() } else { - searchServicesByName(query).first().also { services -> - state = state.copy(result = services.map(contactMapper::fromService).toImmutableList(), searchQuery = query) - } + searchServicesByName(query).first() } + state = state.copy(isLoading = false, searchQuery = query, result = result.map(contactMapper::fromService).toImmutableList()) } } } @@ -85,5 +82,4 @@ data class SearchServicesState( val result: ImmutableList = persistentListOf(), val searchQuery: String = String.EMPTY, val isLoading: Boolean = false, - val error: Boolean = false ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt index f0d221fd2ac..d40a68298d4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt @@ -73,7 +73,7 @@ class SearchUserViewModel @Inject constructor( private val searchQueryTextFlow = MutableStateFlow(String.EMPTY) private val selectedContactsFlow = MutableStateFlow>(persistentSetOf()) - var state: SearchUserState by mutableStateOf(SearchUserState()) + var state: SearchUserState by mutableStateOf(SearchUserState(isLoading = true)) private set init { @@ -114,7 +114,8 @@ class SearchUserViewModel @Inject constructor( contactsResult = newState.contactsResult, publicResult = newState.publicResult, selectedResult = newState.selectedResult, - searchQuery = newState.searchQuery + searchQuery = newState.searchQuery, + isLoading = false, ) } } @@ -180,5 +181,6 @@ data class SearchUserState( val publicResult: ImmutableList = persistentListOf(), val selectedResult: ImmutableList = persistentListOf(), val searchQuery: String = String.EMPTY, - val isOtherDomainAllowed: Boolean = false + val isOtherDomainAllowed: Boolean = false, + val isLoading: Boolean = false, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndServicesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndServicesScreen.kt index 4223f71d9b7..ff3801d6bf9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndServicesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndServicesScreen.kt @@ -166,7 +166,6 @@ fun SearchUsersAndServicesScreen( onOpenUserProfile = onOpenUserProfile, onContactChecked = onContactChecked, isSearchActive = searchBarState.isSearchActive, - isLoading = false, // TODO: update correctly actionType = actionType, lazyListState = lazyListStates[pageIndex], ) @@ -237,7 +236,6 @@ enum class SearchPeopleScreenType { private fun SearchAllPeopleOrContactsScreen( searchQuery: String, contactsSelected: ImmutableSet, - isLoading: Boolean, isSearchActive: Boolean, actionType: ItemActionType, onOpenUserProfile: (Contact) -> Unit, @@ -270,7 +268,7 @@ private fun SearchAllPeopleOrContactsScreen( onOpenUserProfile = onOpenUserProfile, lazyListState = lazyListState, isSearchActive = isSearchActive, - isLoading = isLoading, + isLoading = searchUserViewModel.state.isLoading, actionType = actionType, selectedContactResultsExpanded = selectedContactResultsExpanded, onSelectedContactResultsExpansionChanged = remember { { selectedContactResultsExpanded = it } }, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/widget/SearchFailureWidget.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/widget/SearchFailureWidget.kt index 3765144738d..fadffceff3c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/widget/SearchFailureWidget.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/widget/SearchFailureWidget.kt @@ -20,26 +20,21 @@ package com.wire.android.ui.home.conversations.search.widget import androidx.annotation.StringRes import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp +import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.ui.PreviewMultipleThemes @Composable fun SearchFailureBox(@StringRes failureMessage: Int) { - Box( - Modifier - .fillMaxWidth() - .height(224.dp) - ) { + Box(Modifier.fillMaxSize()) { Text( stringResource(id = failureMessage), modifier = Modifier.align(Alignment.Center), @@ -48,8 +43,8 @@ fun SearchFailureBox(@StringRes failureMessage: Int) { } } -@Preview +@PreviewMultipleThemes @Composable -fun SearchFailureBoxPreview() { +fun SearchFailureBoxPreview() = WireTheme { SearchFailureBox(failureMessage = com.wire.android.R.string.label_no_results_found) } 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 a23a0eaaa8d..b82b6f097c1 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 @@ -16,8 +16,6 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ -@file:OptIn(ExperimentalMaterial3Api::class) - package com.wire.android.ui.userprofile.other import android.annotation.SuppressLint @@ -37,7 +35,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable @@ -79,6 +76,7 @@ import com.wire.android.ui.common.dialogs.BlockUserDialogContent import com.wire.android.ui.common.dialogs.BlockUserDialogState import com.wire.android.ui.common.dialogs.UnblockUserDialogContent import com.wire.android.ui.common.dialogs.UnblockUserDialogState +import com.wire.android.ui.common.dialogs.UserNotFoundDialog import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.spacers.VerticalSpace @@ -216,6 +214,10 @@ fun OtherUserProfileScreen( legalHoldSubjectDialogState::dismiss ) } + + if (viewModel.state.errorLoadingUser != null) { + UserNotFoundDialog(onActionButtonClicked = navigator::navigateBack) + } } @SuppressLint("UnusedCrossfadeTargetStateParameter", "LongParameterList") @@ -612,7 +614,6 @@ enum class OtherUserProfileTabItem(@StringRes val titleResId: Int) : TabItem { override val title: UIText = UIText.StringResource(titleResId) } -@OptIn(ExperimentalMaterial3Api::class) @Composable @PreviewMultipleThemes fun PreviewOtherProfileScreenGroupMemberContent() { @@ -634,7 +635,6 @@ fun PreviewOtherProfileScreenGroupMemberContent() { } } -@OptIn(ExperimentalMaterial3Api::class) @Composable @PreviewMultipleThemes fun PreviewOtherProfileScreenContent() { @@ -657,7 +657,6 @@ fun PreviewOtherProfileScreenContent() { } } -@OptIn(ExperimentalMaterial3Api::class) @Composable @PreviewMultipleThemes fun PreviewOtherProfileScreenContentNotConnected() { @@ -679,7 +678,6 @@ fun PreviewOtherProfileScreenContentNotConnected() { } } -@OptIn(ExperimentalMaterial3Api::class) @Composable @PreviewMultipleThemes fun PreviewOtherProfileScreenTempUser() { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt index caba34eb3b6..f11a08c7030 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt @@ -42,7 +42,6 @@ import com.wire.android.ui.userprofile.group.RemoveConversationMemberState import com.wire.android.ui.userprofile.other.OtherUserProfileInfoMessageType.BlockingUserOperationError import com.wire.android.ui.userprofile.other.OtherUserProfileInfoMessageType.BlockingUserOperationSuccess import com.wire.android.ui.userprofile.other.OtherUserProfileInfoMessageType.ChangeGroupRoleError -import com.wire.android.ui.userprofile.other.OtherUserProfileInfoMessageType.LoadUserInformationError import com.wire.android.ui.userprofile.other.OtherUserProfileInfoMessageType.MutingOperationError import com.wire.android.ui.userprofile.other.OtherUserProfileInfoMessageType.RemoveConversationMemberError import com.wire.android.ui.userprofile.other.OtherUserProfileInfoMessageType.UnblockingUserOperationError @@ -70,8 +69,8 @@ import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStat import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleResult import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase -import com.wire.kalium.logic.feature.e2ei.usecase.IsOtherUserE2EIVerifiedUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificatesUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.IsOtherUserE2EIVerifiedUseCase import com.wire.kalium.logic.feature.user.GetUserInfoResult import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase import dagger.hilt.android.lifecycle.HiltViewModel @@ -189,8 +188,8 @@ class OtherUserProfileScreenViewModel @Inject constructor( .collect { (userResult, groupInfo, oneToOneConversation) -> when (userResult) { is GetUserInfoResult.Failure -> { - appLogger.d("Couldn't not find the user with provided id: $userId") - closeBottomSheetAndShowInfoMessage(LoadUserInformationError) + appLogger.e("Couldn't not find the user with provided id: ${userId.toLogString()}") + updateUserInfoStateForError() } is GetUserInfoResult.Success -> { @@ -370,6 +369,14 @@ class OtherUserProfileScreenViewModel @Inject constructor( } } + private fun updateUserInfoStateForError() { + state = state.copy( + isDataLoading = false, + isAvatarLoading = false, + errorLoadingUser = ErrorLoadingUser.USER_NOT_FOUND + ) + } + private fun updateUserInfoState( userResult: GetUserInfoResult.Success, groupInfo: OtherUserProfileGroupState?, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt index 38900892b7c..491108acc7f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt @@ -54,7 +54,8 @@ data class OtherUserProfileState( val isUnderLegalHold: Boolean = false, val isConversationStarted: Boolean = false, val expiresAt: Instant? = null, - val accentId: Int = -1 + val accentId: Int = -1, + val errorLoadingUser: ErrorLoadingUser? = null ) { fun updateMuteStatus(status: MutedConversationStatus): OtherUserProfileState { return conversationSheetContent?.let { @@ -96,3 +97,8 @@ data class OtherUserProfileGroupState( val isSelfAdmin: Boolean, val conversationId: ConversationId ) + +enum class ErrorLoadingUser { + UNKNOWN, // We might want to expand other errors here as dialogs, ie: federation fallback. + USER_NOT_FOUND, +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt index 326c0226e6c..26b5d29ef57 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt @@ -175,7 +175,7 @@ private fun SelfQRCodeContent( VerticalSpace.x16() Text( modifier = Modifier.padding(horizontal = dimensions().spacing24x), - text = state.userProfileLink, + text = state.userAccountProfileLink, style = MaterialTheme.wireTypography.subline01, color = Color.Black, textAlign = TextAlign.Center @@ -190,7 +190,7 @@ private fun SelfQRCodeContent( color = colorsScheme().secondaryText ) Spacer(modifier = Modifier.weight(1f)) - ShareLinkButton(state.userProfileLink) + ShareLinkButton(state.userAccountProfileLink) VerticalSpace.x8() ShareQRCodeButton { coroutineScope.launch { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeState.kt index 5517e9ffe02..71f6312a0e6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeState.kt @@ -25,5 +25,6 @@ data class SelfQRCodeState( val avatarAsset: UserAvatarAsset? = null, val handle: String = "", val userProfileLink: String = "", + val userAccountProfileLink: String = "", val hasError: Boolean = false ) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt index a996443e7dc..9dc471eb8b8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt @@ -92,21 +92,21 @@ class SelfQRCodeViewModel @Inject constructor( selfQRCodeState = when (val result = selfServerLinks()) { is SelfServerConfigUseCase.Result.Failure -> selfQRCodeState.copy(hasError = true) - is SelfServerConfigUseCase.Result.Success -> generateSelfUserUrl(result.serverLinks.links.accounts) + is SelfServerConfigUseCase.Result.Success -> generateSelfUserUrls(result.serverLinks.links.accounts) } } - private fun generateSelfUserUrl(accountsUrl: String): SelfQRCodeState = + private fun generateSelfUserUrls(accountsUrl: String): SelfQRCodeState = selfQRCodeState.copy( - userProfileLink = String.format(BASE_USER_PROFILE_URL, accountsUrl, selfUserId.value), + userAccountProfileLink = String.format(BASE_USER_PROFILE_URL, accountsUrl, selfUserId.value), + userProfileLink = String.format(DIRECT_BASE_USER_PROFILE_URL, selfUserId.domain, selfUserId.value) ) companion object { const val TEMP_SELF_QR_FILENAME = "temp_self_qr.jpg" const val BASE_USER_PROFILE_URL = "%s/user-profile/?id=%s" - // This URL, can be used when we have a direct link to user profile Milestone2 - const val DIRECT_BASE_USER_PROFILE_URL = "wire://user/%s" + const val DIRECT_BASE_USER_PROFILE_URL = "wire://user/%s/%s" const val QR_QUALITY_COMPRESSION = 80 } } diff --git a/app/src/main/kotlin/com/wire/android/util/debug/FeatureVisibilityFlags.kt b/app/src/main/kotlin/com/wire/android/util/debug/FeatureVisibilityFlags.kt index b363dff9e25..2598c41b22f 100644 --- a/app/src/main/kotlin/com/wire/android/util/debug/FeatureVisibilityFlags.kt +++ b/app/src/main/kotlin/com/wire/android/util/debug/FeatureVisibilityFlags.kt @@ -56,7 +56,7 @@ object FeatureVisibilityFlags { const val MessageEditIcon = true const val SearchConversationMessages = true const val DrawingIcon = true - const val QRCodeEnabled = false + const val QRCodeEnabled = true } val LocalFeatureVisibilityFlags = staticCompositionLocalOf { FeatureVisibilityFlags } diff --git a/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt b/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt index d23e7c38a08..5825b67ed9b 100644 --- a/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt +++ b/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt @@ -25,7 +25,6 @@ import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.feature.SwitchAccountParam import com.wire.android.feature.SwitchAccountResult import com.wire.android.util.EMPTY -import com.wire.android.util.debug.FeatureVisibilityFlags import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.data.id.ConversationId @@ -148,31 +147,18 @@ class DeepLinkProcessor @Inject constructor( } } - /** - * TODO(Rewrite) - * Wait for definitions of Deeplink processing RFC (https://wearezeta.atlassian.net/wiki/x/AgAsWg) - * - * REF: WPB-10532 - */ private fun getConnectingUserProfile(uri: Uri, switchedAccount: Boolean, accountInfo: AccountInfo.Valid): DeepLinkResult { - return if (FeatureVisibilityFlags.QRCodeEnabled) { - // TODO: Wait for definitions of Deeplink processing RFC (https://wearezeta.atlassian.net/wiki/x/AgAsWg) - // TODO: define format of deeplink wire://user/domain/user-id - uri.lastPathSegment?.toDefaultQualifiedId(accountInfo.userId.domain)?.let { - DeepLinkResult.OpenOtherUserProfile(it, switchedAccount) - } - DeepLinkResult.Unknown - } else { - DeepLinkResult.Unknown - } + // todo. handle with domain case, before lastPathSegment. format of deeplink wire://user/domain/user-id + return uri.lastPathSegment?.toDefaultQualifiedId(accountInfo.userId.domain)?.let { + DeepLinkResult.OpenOtherUserProfile(it, switchedAccount) + } ?: return DeepLinkResult.Unknown } /** - * TODO(Rewrite) - * Wait for definitions of Deeplink processing RFC (https://wearezeta.atlassian.net/wiki/x/AgAsWg) - * i.e. Define format of deeplink wire://user/domain/user-id + * Converts the string to a [QualifiedID] with the current user domain or default, to preserve retro compatibility. + * When implementing Milestone 2 this should be replaced with a new qualifiedIdMapper, implementing wire://user/domain/user-id * - * REF: WPB-10532 + * - new mapper should follow "domain/user-id" parsing. */ private fun String.toDefaultQualifiedId(currentUserDomain: String?): QualifiedID { val domain = currentUserDomain ?: "wire.com" diff --git a/app/src/main/play/release-notes/en-US/default.txt b/app/src/main/play/release-notes/en-US/default.txt index c0bced40fd6..dd3380e7793 100644 --- a/app/src/main/play/release-notes/en-US/default.txt +++ b/app/src/main/play/release-notes/en-US/default.txt @@ -1,16 +1,18 @@ -New -- Fresh design - completely rebuilt -- Add Reactions to messages -- Swipe left to reply -- Scroll to end button -- Navigate to original message from a reply -- Accessible main navigation & updated conversation list with activity section -- Improved conversation search -- Easier 1:1 & group creation -- Group chats have distinct colors -- Call controls always accessible -- Text field adjusts for longer messages +New: +- Survey on call quality +- Send a GIF from the keyboard +- Call in a separate window +- Compatibility with Android 15 +- Confirmation before pinging in groups +- Preparation for private to cloud federation +- Preparation for MLS as the new standard protocol -Improved -- Reliable notifications -- Better visual contrast (WCAG 2.1 AA) \ No newline at end of file +Improvements: +- Performance +- Switch account from a notification during a call and the call remains +- Video pausing while scrolling + +Fixes: +- People could not see active speakers +- People could not edit their email +- Several crashes diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 146db56e9fb..0e080e0598c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -982,6 +982,8 @@ Ignore Get certainty about the identity of %s\'s before connecting. Please verify the person\'s identity before accepting the connection request. + Wire can\'t find this person + You may not have permission with this account or the person may not be on Wire. Unable to start conversation You can\'t start the conversation with %1$s right now. %1$s needs to open Wire or log in again first. Please try again later. diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt index d12f396c520..3a9085cfd6e 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt @@ -33,9 +33,14 @@ class SelfQRCodeViewModelTest { // when - then assertEquals( - expected = "${ServerConfig.STAGING.accounts}/user-profile/?id=${TestUser.SELF_USER.id.value}", + expected = "wire://user/${TestUser.SELF_USER.id.domain}/${TestUser.SELF_USER.id.value}", actual = viewModel.selfQRCodeState.userProfileLink, ) + + assertEquals( + expected = "${ServerConfig.STAGING.accounts}/user-profile/?id=${TestUser.SELF_USER.id.value}", + actual = viewModel.selfQRCodeState.userAccountProfileLink, + ) } private class Arrangement { diff --git a/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt b/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt index 11f9207ceca..f6d66c47715 100644 --- a/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt +++ b/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt @@ -244,6 +244,20 @@ class DeepLinkProcessorTest { assertEquals(DeepLinkResult.SharingIntent, result) } + @Test + fun `given an other profile deeplink from QR code, returns Conversation with conversationId`() = runTest { + val (arrangement, deepLinkProcessor) = Arrangement() + .withOtherUserProfileQRDeepLink(userIdToOpen = OTHER_USER_ID, userId = CURRENT_USER_ID) + .withCurrentSessionSuccess(CURRENT_USER_ID) + .arrange() + val conversationResult = deepLinkProcessor(arrangement.uri, false) + assertInstanceOf(DeepLinkResult.OpenOtherUserProfile::class.java, conversationResult) + assertEquals( + DeepLinkResult.OpenOtherUserProfile(UserId("other_user", "domain"), false), + conversationResult + ) + } + class Arrangement { @MockK @@ -318,6 +332,12 @@ class DeepLinkProcessorTest { coEvery { uri.getQueryParameter(DeepLinkProcessor.USER_TO_USE_QUERY_PARAM) } returns userId.toString() } + fun withOtherUserProfileQRDeepLink(userIdToOpen: UserId = OTHER_USER_ID, userId: UserId = CURRENT_USER_ID) = apply { + coEvery { uri.host } returns DeepLinkProcessor.OPEN_USER_PROFILE_DEEPLINK_HOST + coEvery { uri.lastPathSegment } returns userIdToOpen.value + coEvery { uri.getQueryParameter(DeepLinkProcessor.USER_TO_USE_QUERY_PARAM) } returns userId.toString() + } + fun withCurrentSession(result: CurrentSessionResult) = apply { coEvery { currentSession() } returns result }