diff --git a/app/src/main/java/tech/relaycorp/letro/conversation/viewing/ConversationViewModel.kt b/app/src/main/java/tech/relaycorp/letro/conversation/viewing/ConversationViewModel.kt index 43069e62..30f306dd 100644 --- a/app/src/main/java/tech/relaycorp/letro/conversation/viewing/ConversationViewModel.kt +++ b/app/src/main/java/tech/relaycorp/letro/conversation/viewing/ConversationViewModel.kt @@ -4,18 +4,22 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import tech.relaycorp.letro.contacts.storage.repository.ContactsRepository import tech.relaycorp.letro.conversation.model.ExtendedConversation import tech.relaycorp.letro.conversation.storage.repository.ConversationsRepository import tech.relaycorp.letro.ui.navigation.Route +import tech.relaycorp.letro.utils.ext.emitOn import javax.inject.Inject @HiltViewModel class ConversationViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private val conversationsRepository: ConversationsRepository, + private val contactsRepository: ContactsRepository, ) : ViewModel() { private val conversationId: String = savedStateHandle[Route.Conversation.KEY_CONVERSATION_ID]!! @@ -26,10 +30,23 @@ class ConversationViewModel @Inject constructor( val deleteConversationDialogState: StateFlow get() = _deleteConversationDialogState + + private val _showNoContactsSnackbarSignal: MutableSharedFlow = MutableSharedFlow() + val showNoContactsSnackbarSignal: MutableSharedFlow + get() = _showNoContactsSnackbarSignal + init { conversationsRepository.markConversationAsRead(conversationId) } + fun canReply(): Boolean { + val canReply = contactsRepository.contactsState.value.isPairedContactExist + if (!canReply) { + _showNoContactsSnackbarSignal.emitOn(Unit, viewModelScope) + } + return canReply + } + fun onDeleteConversationClick() { _deleteConversationDialogState.update { it.copy( diff --git a/app/src/main/java/tech/relaycorp/letro/conversation/viewing/ui/ConversationScreen.kt b/app/src/main/java/tech/relaycorp/letro/conversation/viewing/ui/ConversationScreen.kt index 3775b751..1e169e18 100644 --- a/app/src/main/java/tech/relaycorp/letro/conversation/viewing/ui/ConversationScreen.kt +++ b/app/src/main/java/tech/relaycorp/letro/conversation/viewing/ui/ConversationScreen.kt @@ -61,6 +61,7 @@ fun ConversationScreen( onConversationArchived: (Boolean) -> Unit, onBackClicked: () -> Unit, onAttachmentClick: (UUID) -> Unit, + showAddContactSnackbar: () -> Unit, viewModel: ConversationViewModel = hiltViewModel(), ) { val scrollState = rememberLazyListState() @@ -70,6 +71,12 @@ fun ConversationScreen( val deleteConversationDialogState by viewModel.deleteConversationDialogState.collectAsState() + LaunchedEffect(Unit) { + viewModel.showNoContactsSnackbarSignal.collect { + showAddContactSnackbar() + } + } + if (conversation != null) { LaunchedEffect(Unit) { // Scroll to the top of a conversation on screen opening scrollState.scrollToItem(conversation.messages.size - 1) @@ -97,7 +104,11 @@ fun ConversationScreen( ) { ConversationToolbar( isArchived = conversation.isArchived, - onReplyClick = onReplyClick, + onReplyClick = { + if (viewModel.canReply()) { + onReplyClick() + } + }, onBackClicked = onBackClicked, onArchiveClick = { val isArchived = viewModel.onArchiveConversationClicked() diff --git a/app/src/main/java/tech/relaycorp/letro/main/MainViewModel.kt b/app/src/main/java/tech/relaycorp/letro/main/MainViewModel.kt index 0d0e58da..97579ee7 100644 --- a/app/src/main/java/tech/relaycorp/letro/main/MainViewModel.kt +++ b/app/src/main/java/tech/relaycorp/letro/main/MainViewModel.kt @@ -25,6 +25,7 @@ import tech.relaycorp.letro.contacts.storage.repository.ContactsRepository import tech.relaycorp.letro.conversation.attachments.AttachmentsRepository import tech.relaycorp.letro.conversation.attachments.filepicker.FileConverter import tech.relaycorp.letro.conversation.attachments.filepicker.model.File +import tech.relaycorp.letro.conversation.storage.repository.ConversationsRepository import tech.relaycorp.letro.main.di.TermsAndConditionsLink import tech.relaycorp.letro.push.model.PushAction import tech.relaycorp.letro.ui.navigation.RootNavigationScreen @@ -40,6 +41,7 @@ class MainViewModel @Inject constructor( private val contactsRepository: ContactsRepository, private val attachmentsRepository: AttachmentsRepository, private val fileConverter: FileConverter, + private val conversationsRepository: ConversationsRepository, @TermsAndConditionsLink private val termsAndConditionsLink: String, ) : ViewModel() { @@ -94,8 +96,9 @@ class MainViewModel @Inject constructor( accountRepository.currentAccount, contactsRepository.contactsState, awalaManager.awalaInitializationState, - ) { currentAccount, contactsState, awalaInitializationState -> - Log.d(TAG, "$currentAccount; $contactsState; $awalaInitializationState") + conversationsRepository.conversations, + ) { currentAccount, contactsState, awalaInitializationState, conversations -> + Log.d(TAG, "$currentAccount; $contactsState; $awalaInitializationState; ${conversations.size}") when { awalaInitializationState == AwalaInitializationState.AWALA_NOT_INSTALLED -> RootNavigationScreen.AwalaNotInstalled awalaInitializationState == AwalaInitializationState.INITIALIZATION_NONFATAL_ERROR -> RootNavigationScreen.AwalaInitializationError(isFatal = false) @@ -104,7 +107,7 @@ class MainViewModel @Inject constructor( currentAccount == null -> RootNavigationScreen.Registration !currentAccount.isCreated -> RootNavigationScreen.RegistrationWaiting !contactsState.isPairRequestWasEverSent -> RootNavigationScreen.WelcomeToLetro - !contactsState.isPairedContactExist -> RootNavigationScreen.NoContactsScreen + !contactsState.isPairedContactExist && conversations.isEmpty() -> RootNavigationScreen.NoContactsScreen else -> RootNavigationScreen.Home } } diff --git a/app/src/main/java/tech/relaycorp/letro/main/home/HomeViewModel.kt b/app/src/main/java/tech/relaycorp/letro/main/home/HomeViewModel.kt index d8aca1c3..0fa8e454 100644 --- a/app/src/main/java/tech/relaycorp/letro/main/home/HomeViewModel.kt +++ b/app/src/main/java/tech/relaycorp/letro/main/home/HomeViewModel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import tech.relaycorp.letro.contacts.storage.repository.ContactsRepository import tech.relaycorp.letro.main.home.badge.UnreadBadgesManager import tech.relaycorp.letro.main.home.ui.HomeFloatingActionButtonConfig import javax.inject.Inject @@ -17,6 +18,7 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val unreadBadgesManager: UnreadBadgesManager, + private val contactsRepository: ContactsRepository, ) : ViewModel() { private val _uiState = MutableStateFlow(HomeUiState()) @@ -27,6 +29,10 @@ class HomeViewModel @Inject constructor( val createNewConversationSignal: SharedFlow get() = _createNewConversationSignal + private val _showNoContactsSnackbarSignal: MutableSharedFlow = MutableSharedFlow() + val showNoContactsSnackbarSignal: MutableSharedFlow + get() = _showNoContactsSnackbarSignal + init { viewModelScope.launch { unreadBadgesManager.unreadConversations.collect { @@ -70,7 +76,11 @@ class HomeViewModel @Inject constructor( viewModelScope.launch { when (uiState.value.currentTab) { TAB_CHATS -> { - _createNewConversationSignal.emit(Unit) + if (contactsRepository.contactsState.value.isPairedContactExist) { + _createNewConversationSignal.emit(Unit) + } else { + _showNoContactsSnackbarSignal.emit(Unit) + } } TAB_CONTACTS -> { _uiState.update { diff --git a/app/src/main/java/tech/relaycorp/letro/ui/common/LetroButton.kt b/app/src/main/java/tech/relaycorp/letro/ui/common/LetroButton.kt index 7f4f2833..e547d3d7 100644 --- a/app/src/main/java/tech/relaycorp/letro/ui/common/LetroButton.kt +++ b/app/src/main/java/tech/relaycorp/letro/ui/common/LetroButton.kt @@ -41,7 +41,7 @@ fun LetroButton( }, contentColor = when (buttonType) { ButtonType.Filled -> MaterialTheme.colorScheme.onPrimary - ButtonType.Outlined -> MaterialTheme.colorScheme.onSurface + ButtonType.Outlined -> MaterialTheme.colorScheme.primary }, disabledContainerColor = LetroColor.disabledButtonBackgroundColor(), disabledContentColor = LetroColor.disabledButtonTextColor(), @@ -71,7 +71,6 @@ fun LetroButton( Text( text = text, style = MaterialTheme.typography.LabelLargeProminent, - color = contentColor, ) } } diff --git a/app/src/main/java/tech/relaycorp/letro/ui/navigation/LetroNavHost.kt b/app/src/main/java/tech/relaycorp/letro/ui/navigation/LetroNavHost.kt index d2fe4577..145226ba 100644 --- a/app/src/main/java/tech/relaycorp/letro/ui/navigation/LetroNavHost.kt +++ b/app/src/main/java/tech/relaycorp/letro/ui/navigation/LetroNavHost.kt @@ -37,6 +37,7 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.google.accompanist.systemuicontroller.SystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -119,6 +120,20 @@ fun LetroNavHost( } } + LaunchedEffect(Unit) { + homeViewModel.showNoContactsSnackbarSignal.collect { + showSnackbar( + scope = this, + snackbarHostState = snackbarHostState, + message = stringsProvider.snackbar.youNeedAtLeastOneContact, + actionLabel = stringsProvider.snackbar.addContact, + onActionPerformed = { + navController.navigate(Route.ManageContact.getRouteName(ManageContactViewModel.Type.NEW_CONTACT)) + } + ) + } + } + LaunchedEffect(isHomeScreenInitialized) { if (!isHomeScreenInitialized) { return@LaunchedEffect @@ -283,7 +298,7 @@ fun LetroNavHost( val result = snackbarHostState.showSnackbar( message = stringsProvider.snackbar.notificationPermissionDenied, actionLabel = stringsProvider.snackbar.goToSettings, - duration = SnackbarDuration.Long, + duration = SnackbarDuration.Short, ) if (result == SnackbarResult.ActionPerformed) { onGoToNotificationsSettingsClick() @@ -391,6 +406,17 @@ fun LetroNavHost( onAttachmentClick = { fileId -> mainViewModel.onAttachmentClick(fileId) }, + showAddContactSnackbar = { + showSnackbar( + scope = scope, + snackbarHostState = snackbarHostState, + message = stringsProvider.snackbar.youNoLongerConnected, + actionLabel = stringsProvider.snackbar.addContact, + onActionPerformed = { + navController.navigate(Route.ManageContact.getRouteName(ManageContactViewModel.Type.NEW_CONTACT)) + } + ) + } ) } composable(Route.Settings.name) { @@ -469,6 +495,25 @@ fun LetroNavHost( ) } +private fun showSnackbar( + scope: CoroutineScope, + snackbarHostState: SnackbarHostState, + message: String, + actionLabel: String, + onActionPerformed: () -> Unit, +) { + scope.launch { + val result = snackbarHostState.showSnackbar( + message = message, + actionLabel = actionLabel, + duration = SnackbarDuration.Short, + ) + if (result == SnackbarResult.ActionPerformed) { + onActionPerformed() + } + } +} + private fun handleFirstNavigation( navController: NavHostController, firstNavigation: RootNavigationScreen, diff --git a/app/src/main/java/tech/relaycorp/letro/ui/utils/SnackbarStringsProvider.kt b/app/src/main/java/tech/relaycorp/letro/ui/utils/SnackbarStringsProvider.kt index 4b78d615..b5cc4257 100644 --- a/app/src/main/java/tech/relaycorp/letro/ui/utils/SnackbarStringsProvider.kt +++ b/app/src/main/java/tech/relaycorp/letro/ui/utils/SnackbarStringsProvider.kt @@ -15,6 +15,9 @@ interface SnackbarStringsProvider { val notificationPermissionDenied: String val goToSettings: String val accountDeleted: String + val youNeedAtLeastOneContact: String + val youNoLongerConnected: String + val addContact: String } class SnackbarStringsProviderImpl @Inject constructor( @@ -46,4 +49,13 @@ class SnackbarStringsProviderImpl @Inject constructor( override val accountDeleted: String get() = activity.getString(R.string.account_deleted) + + override val youNeedAtLeastOneContact: String + get() = activity.getString(R.string.you_need_at_least_one_contact) + + override val addContact: String + get() = activity.getString(R.string.general_pair_with_others) + + override val youNoLongerConnected: String + get() = activity.getString(R.string.you_cannot_reply_not_connected) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a597abf2..fa4017d6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -158,6 +158,9 @@ Switch accounts Selected item Unselected item + + You need at least one contact to start a conversation + You can\'t reply to this user because you\'re no longer connected. %s MB %s KB