diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ce76de39a8dfd5d9a6dcffa07a3b37de2721bd34..14daa9aa93efc2020a96f0e7ded0931840699bf0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -56,9 +56,18 @@ android:theme="@style/SplashTheme"> <intent-filter> <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> + <intent-filter android:autoVerify="true"> + <category android:name="android.intent.category.BROWSABLE" /> + <category android:name="android.intent.category.DEFAULT" /> + <action android:name="android.intent.action.VIEW" /> + <data + android:scheme="https" + android:host="elixxir.io" + android:pathPrefix="/connect" + /> + </intent-filter> </activity> <activity android:name=".ui.intro.splash.SplashScreenLoadingActivity" @@ -84,9 +93,7 @@ android:launchMode="singleTask"> <intent-filter> <data android:scheme="db-${dropboxKey}" /> - <action android:name="android.intent.action.VIEW" /> - <category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> diff --git a/app/src/main/java/io/xxlabs/messenger/notifications/MessagingService.kt b/app/src/main/java/io/xxlabs/messenger/notifications/MessagingService.kt index c098e3e84d90ae85339ec8be3a8bd2c70488e355..8cbd1f143de2593be126e3a6c2574ad5c1697fc9 100644 --- a/app/src/main/java/io/xxlabs/messenger/notifications/MessagingService.kt +++ b/app/src/main/java/io/xxlabs/messenger/notifications/MessagingService.kt @@ -5,7 +5,6 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Intent -import android.media.RingtoneManager.isDefault import android.os.Build import android.os.Bundle import android.os.PowerManager @@ -28,7 +27,7 @@ import io.xxlabs.messenger.requests.ui.RequestsFragment import io.xxlabs.messenger.support.util.value import io.xxlabs.messenger.ui.intro.splash.SplashScreenPlaceholderActivity import io.xxlabs.messenger.ui.main.MainActivity -import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_DEEP_LINK_BUNDLE +import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_NOTIFICATION_CLICK import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_GROUP_CHAT import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_PRIVATE_CHAT import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_REQUEST @@ -196,7 +195,7 @@ class MessagingService : FirebaseMessagingService(), HasAndroidInjector { } } } - intent.putExtra(INTENT_DEEP_LINK_BUNDLE, deepLinkBundle) + intent.putExtra(INTENT_NOTIFICATION_CLICK, deepLinkBundle) return intent } diff --git a/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt b/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt index 4fb123e5e9991d3937cb78b0a51f3ae0b638c9d0..5d3d5c48905e602ac92ca6f627cfcdc43a754c38 100644 --- a/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt +++ b/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt @@ -38,6 +38,7 @@ import io.xxlabs.messenger.repository.DaoRepository import io.xxlabs.messenger.repository.PreferencesRepository import io.xxlabs.messenger.repository.base.BasePreferences import io.xxlabs.messenger.repository.base.BaseRepository +import io.xxlabs.messenger.repository.client.NodeErrorException.Companion.isNodeError import io.xxlabs.messenger.support.appContext import io.xxlabs.messenger.support.extensions.fromBase64toByteArray import io.xxlabs.messenger.support.extensions.toBase64String @@ -953,7 +954,13 @@ class ClientRepository @Inject constructor( override fun areNodesReady(): Boolean = recursiveAreNodesReady() private fun recursiveAreNodesReady(retries: Int = 0): Boolean { - val status = clientWrapper.getNodeRegistrationStatus() + val status = try { + clientWrapper.getNodeRegistrationStatus() + } catch (e: Exception) { + if (e.isNodeError()) return false + else throw e + } + val rate: Double = ((status.first.toDouble() / status.second)) Timber.v("[NODE REGISTRATION STATUS] Registration rate: $rate") @@ -1054,4 +1061,10 @@ class ClientRepository @Inject constructor( } -class NodeErrorException : Exception() \ No newline at end of file +class NodeErrorException : Exception() { + companion object { + fun Exception.isNodeError(): Boolean = + message?.contains("network is not healthy", false) + ?: false + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xxlabs/messenger/search/FactSearchFragment.kt b/app/src/main/java/io/xxlabs/messenger/search/FactSearchFragment.kt index 2a2f1255dd3b144ead0f4f0cd7fd58f840c04ef7..5b5e87be38e3cdadf3c5a8f8dc16d068b0f5f459 100644 --- a/app/src/main/java/io/xxlabs/messenger/search/FactSearchFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/search/FactSearchFragment.kt @@ -1,6 +1,8 @@ package io.xxlabs.messenger.search import android.os.Bundle +import android.text.Editable +import android.text.SpannableString import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -43,7 +45,7 @@ abstract class FactSearchFragment : Fragment(), Injectable { protected val resultsAdapter: RequestsAdapter by lazy { RequestsAdapter(requestsViewModel) } - private lateinit var binding: FragmentFactSearchBinding + protected lateinit var binding: FragmentFactSearchBinding override fun onCreateView( inflater: LayoutInflater, @@ -51,13 +53,6 @@ abstract class FactSearchFragment : Fragment(), Injectable { savedInstanceState: Bundle? ): View { binding = FragmentFactSearchBinding.inflate(inflater, container, false) -// lifecycleScope.launch { -// repeatOnLifecycle(Lifecycle.State.STARTED) { -// getResults().collect { results -> -// resultsAdapter.submitList(results) -// } -// } -// } binding.lifecycleOwner = viewLifecycleOwner return binding.root } @@ -121,6 +116,17 @@ class UsernameSearchFragment : FactSearchFragment() { } override fun getSearchTabUi(): FactSearchUi = searchViewModel.usernameSearchUi + + override fun onResume() { + super.onResume() + searchViewModel.invitationFrom.observe(viewLifecycleOwner) { username -> + username?.let { + binding.searchTextInputEditText.setText(username) + onSearchClicked(username) + searchViewModel.onInvitationHandled() + } + } + } } class EmailSearchFragment : FactSearchFragment() { diff --git a/app/src/main/java/io/xxlabs/messenger/search/UserSearchFragment.kt b/app/src/main/java/io/xxlabs/messenger/search/UserSearchFragment.kt index fef9c04a7f18959da549cbc09a497a1ed6261c94..59ef598bbaaa39d9cee5892050867dfa3e609a85 100644 --- a/app/src/main/java/io/xxlabs/messenger/search/UserSearchFragment.kt +++ b/app/src/main/java/io/xxlabs/messenger/search/UserSearchFragment.kt @@ -52,6 +52,9 @@ class UserSearchFragment : RequestsFragment() { override val navController: NavController by lazy { findNavController() } + private val invitationUsername: String? by lazy { + UserSearchFragmentArgs.fromBundle(requireArguments()).username + } override fun onAttach(context: Context) { super.onAttach(context) @@ -163,9 +166,16 @@ class UserSearchFragment : RequestsFragment() { override fun onStart() { super.onStart() + handleInvitation() observeUi() } + private fun handleInvitation() { + invitationUsername?.let { + searchViewModel.onInvitationReceived(it) + } + } + override fun onResume() { super.onResume() resetTabPosition() diff --git a/app/src/main/java/io/xxlabs/messenger/search/UserSearchViewModel.kt b/app/src/main/java/io/xxlabs/messenger/search/UserSearchViewModel.kt index e5f40eaaf6edbe38ac692acc677c95f1ed6e669c..47947cb6e230838a98817ee1c3c4e369311a361a 100644 --- a/app/src/main/java/io/xxlabs/messenger/search/UserSearchViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/search/UserSearchViewModel.kt @@ -17,10 +17,10 @@ import io.xxlabs.messenger.data.room.model.ContactData import io.xxlabs.messenger.repository.DaoRepository import io.xxlabs.messenger.repository.PreferencesRepository import io.xxlabs.messenger.repository.base.BaseRepository +import io.xxlabs.messenger.repository.client.NodeErrorException import io.xxlabs.messenger.requests.data.contact.ContactRequestData import io.xxlabs.messenger.requests.data.contact.ContactRequestsRepository import io.xxlabs.messenger.requests.model.ContactRequest -import io.xxlabs.messenger.requests.model.Request import io.xxlabs.messenger.requests.ui.list.adapter.* import io.xxlabs.messenger.support.appContext import io.xxlabs.messenger.support.toast.ToastUI @@ -44,6 +44,10 @@ class UserSearchViewModel @Inject constructor( private val requestsDataSource: ContactRequestsRepository, ): ViewModel(){ + private val genericSearchError: String by lazy { + appContext().getString(R.string.search_generic_error_message) + } + var previousTabPosition: Int = UserSearchFragment.SEARCH_USERNAME private val initialState: SearchUiState by lazy { @@ -206,6 +210,9 @@ class UserSearchViewModel @Inject constructor( private var searchJob: Job? = null + val invitationFrom: LiveData<String?> by ::_invitationFrom + private val _invitationFrom = MutableLiveData<String?>(null) + init { showNewUserPopups() } @@ -234,7 +241,7 @@ class UserSearchViewModel @Inject constructor( val notificationToken = enableNotifications() onNotificationsEnabled(notificationToken) } catch (e: Exception) { - showToast( + showError( e.message ?: "Failed to enable notifications. Please try again in Settings." ) } @@ -271,6 +278,14 @@ class UserSearchViewModel @Inject constructor( repo.enableDummyTraffic(enabled) } + fun onInvitationReceived(username: String) { + _invitationFrom.value = username + } + + fun onInvitationHandled() { + _invitationFrom.value = null + } + suspend fun onUsernameSearch(username: String?): Flow<List<RequestItem>> { _usernameResults.value = listOf() val factQuery = FactQuery.UsernameQuery(username) @@ -294,9 +309,8 @@ class UserSearchViewModel @Inject constructor( searchJob?.cancel() searchJob = coroutineContext.job - if (!isValidQuery(factQuery)) flowOf(listOf<RequestItem?>()) - - _udSearchUi.value = searchRunningState + if (!isValidQuery(factQuery)) return flowOf(listOf()) + changeStateTo(searchRunningState) return combine( searchUd(factQuery), @@ -333,7 +347,6 @@ class UserSearchViewModel @Inject constructor( listOf(ud) + foundRequests.sortedBy { it.username } } - if (nonConnections.isEmpty()) { // If there's no UD result or Requests, just show the Connections with no divider. foundConnections.toList().sortedBy { @@ -360,7 +373,7 @@ class UserSearchViewModel @Inject constructor( get() = (request as? ContactRequest)?.model?.username ?: "" private suspend fun allRequests(): Flow<List<RequestItem>> = - requestsDataSource.getRequests().mapNotNull { requestsList -> + requestsDataSource.getRequests().map { requestsList -> requestsList.map { it.asRequestSearchResult() } @@ -370,6 +383,7 @@ class UserSearchViewModel @Inject constructor( val connectionsList = savedUsers().filter { it.isConnection() }.asConnectionsSearchResult() + emit(connectionsList) }.stateIn(viewModelScope) @@ -428,6 +442,9 @@ class UserSearchViewModel @Inject constructor( text = "There are no users with that ${factQuery.type.name.lowercase()}." ) + private fun couldNotCompletePlaceholder(error: String): RequestItem = + EmptyPlaceholderItem(text = error) + private suspend fun savedUsers(): List<ContactData> = daoRepo.getAllContacts().value() @@ -583,30 +600,42 @@ class UserSearchViewModel @Inject constructor( private suspend fun searchUd(factQuery: FactQuery) = flow { val result = try { - val udResult = repo.searchUd(factQuery.fact, factQuery.type).value() + val udResult = fetchUser(factQuery) udResult.second?.let { // Error message if (it.isNotEmpty()) { if (!it.contains("no results found", true)) { - showToast(it) + showError(it) } - _udSearchUi.value = searchCompleteState noResultPlaceholder(factQuery) } else { // Search result - _udSearchUi.value = searchCompleteState udResult.first?.asSearchResult() ?: noResultPlaceholder(factQuery) } } ?: run { - _udSearchUi.value = searchCompleteState udResult.first?.asSearchResult() ?: noResultPlaceholder(factQuery) } } catch (e: Exception) { - e.message?.let { showToast(it) } - _udSearchUi.value = searchCompleteState - noResultPlaceholder(factQuery) + e.message?.let { + showError(genericSearchError) + couldNotCompletePlaceholder(it) + } ?: run { + couldNotCompletePlaceholder(genericSearchError) + } } + + changeStateTo(searchCompleteState) + emit(result) }.stateIn(viewModelScope) + private suspend fun fetchUser(factQuery: FactQuery): Pair<ContactWrapperBase?, String?> { + return try { + repo.searchUd(factQuery.fact, factQuery.type).value() + } catch (e: NodeErrorException) { + delay(5000) + fetchUser(factQuery) + } + } + private fun ContactWrapperBase.asSearchResult(): RequestItem { // ContactWrapperBase -> ContactRequestData val requestData = ContactRequestData( @@ -616,7 +645,11 @@ class UserSearchViewModel @Inject constructor( return SearchResultItem(requestData) } - private fun showToast(error: String) { + private fun changeStateTo(ui: UdSearchUi) { + _udSearchUi.postValue(ui) + } + + private fun showError(error: String) { _toastUi.postValue( ToastUI.create( body = error, @@ -643,7 +676,7 @@ class UserSearchViewModel @Inject constructor( private fun onCancelSearchClicked() { searchJob?.cancel() - _udSearchUi.value = searchCompleteState + changeStateTo(searchCompleteState) } private fun onCountryCodeClicked() { @@ -663,9 +696,10 @@ class UserSearchViewModel @Inject constructor( } fun onUserInput(input: String?) { - _udSearchUi.value = input?.let { - userInputState - } ?: initialState + _udSearchUi.value = + input?.let { + userInputState + } ?: initialState } } diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/splash/SplashScreenPlaceholderActivity.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/splash/SplashScreenPlaceholderActivity.kt index a1dee2d293d8b04735b91a4cb2f092f80818fa1d..68bc86051046701fa28bb73cc400d609aed2159d 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/intro/splash/SplashScreenPlaceholderActivity.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/splash/SplashScreenPlaceholderActivity.kt @@ -19,7 +19,9 @@ import io.xxlabs.messenger.support.isMockVersion import io.xxlabs.messenger.support.util.Utils import io.xxlabs.messenger.ui.base.BaseInjectorActivity import io.xxlabs.messenger.ui.main.MainActivity -import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_DEEP_LINK_BUNDLE +import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_INVITATION +import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_NOTIFICATION_CLICK +import timber.log.Timber import javax.inject.Inject class SplashScreenPlaceholderActivity : BaseInjectorActivity() { @@ -54,13 +56,33 @@ class SplashScreenPlaceholderActivity : BaseInjectorActivity() { } private fun handleIntent(intent: Intent) { + if (Intent.ACTION_VIEW == intent.action) { + // Implicit Intent from an invitation link + intent.data?.getQueryParameter("username")?.let { username -> + invitationIntent(username) + } + } else notificationIntent(intent) + } + + private fun invitationIntent(username: String) { + // Invitations can only be handled if the user has an account. + if (preferencesRepository.name.isNotEmpty()) { + val intent = Intent(this, MainActivity::class.java).apply { + putExtra(INTENT_INVITATION, username) + } + startActivity(intent) + finish() + } + } + + private fun notificationIntent(intent: Intent) { // Pass this intent on to MainActivity. - mainIntent = intent.getBundleExtra(INTENT_DEEP_LINK_BUNDLE)?.let { + mainIntent = intent.getBundleExtra(INTENT_NOTIFICATION_CLICK)?.let { Intent( this@SplashScreenPlaceholderActivity, MainActivity::class.java ).apply { - putExtra(INTENT_DEEP_LINK_BUNDLE, it) + putExtra(INTENT_NOTIFICATION_CLICK, it) } } } diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/MainActivity.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/MainActivity.kt index c6eb30feb4e14a45a60a7653876734b8c099f286..e6edbd543413526a2e6d2872c43e243821756a7d 100755 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/MainActivity.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/MainActivity.kt @@ -4,7 +4,6 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator import android.annotation.SuppressLint -import android.app.ProgressDialog.show import android.content.Context import android.content.Intent import android.graphics.BitmapFactory @@ -12,6 +11,7 @@ import android.graphics.Color import android.os.Bundle import android.view.* import android.view.inputmethod.InputMethodManager +import android.widget.ProgressBar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.os.bundleOf import androidx.core.view.WindowInsetsCompat @@ -20,12 +20,12 @@ import androidx.lifecycle.* import androidx.lifecycle.Observer import androidx.navigation.NavController import androidx.navigation.Navigation +import androidx.navigation.findNavController import androidx.navigation.ui.onNavDestinationSelected import com.bumptech.glide.Glide import com.google.android.material.shape.CornerFamily import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_INDEFINITE -import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG import com.google.android.material.snackbar.Snackbar import io.xxlabs.messenger.BuildConfig import io.xxlabs.messenger.NavMainDirections @@ -33,11 +33,9 @@ import io.xxlabs.messenger.R import io.xxlabs.messenger.bindings.wrapper.bindings.BindingsWrapperBindings import io.xxlabs.messenger.data.data.DataRequestState import io.xxlabs.messenger.data.data.SimpleRequestState -import io.xxlabs.messenger.data.datatype.NetworkState import io.xxlabs.messenger.data.room.model.Contact import io.xxlabs.messenger.databinding.ComponentCustomToastBinding import io.xxlabs.messenger.media.MediaProviderActivity -import io.xxlabs.messenger.notifications.MessagingService import io.xxlabs.messenger.support.callback.NetworkWatcher import io.xxlabs.messenger.support.dialog.PopupActionBottomDialog import io.xxlabs.messenger.support.extensions.* @@ -53,15 +51,16 @@ import io.xxlabs.messenger.ui.base.BaseFragment import io.xxlabs.messenger.ui.global.BaseInstance import io.xxlabs.messenger.ui.global.ContactsViewModel import io.xxlabs.messenger.ui.global.NetworkViewModel +import io.xxlabs.messenger.ui.main.chat.setVisibility import io.xxlabs.messenger.ui.main.chats.ChatsFragment import io.xxlabs.messenger.ui.main.chats.ChatsViewModel import io.xxlabs.messenger.ui.main.contacts.PhotoSelectorFragment import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.component_menu.* +import kotlinx.android.synthetic.main.fragment_delete_account.* import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber -import java.util.* import javax.inject.Inject private val Bundle.isPrivateMessage: Boolean @@ -88,6 +87,7 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActiv var isBackBtnAllowed = true var isMenuOpened = false + private val intentQueue: MutableList<Intent> = mutableListOf() override fun onStart() { super.onStart() @@ -167,13 +167,33 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActiv handleIntent(intent) } + private fun handleIntent(intent: Intent) { - intent.getBundleExtra(INTENT_DEEP_LINK_BUNDLE)?.let { - handleDeepLink(it) + if (mainViewModel.areComponentsInitialized.value == true) { + intent.getBundleExtra(INTENT_NOTIFICATION_CLICK)?.let { + // PendingIntent from notifications + handleNotification(it) + return + } + + intent.getStringExtra(INTENT_INVITATION)?.let { username -> + // Implicit intent from an invitation link + invitationIntent(username) + return + } + } else { + intentQueue.add(intent) } } - private fun handleDeepLink(bundle: Bundle) { + private fun invitationIntent(username: String) { + val userSearch = NavMainDirections.actionGlobalConnectionInvitation().apply { + this.username = username + } + mainNavController.navigateSafe(userSearch) + } + + private fun handleNotification(bundle: Bundle) { with (bundle) { when { isPrivateMessage -> privateMessageIntent(this) @@ -361,6 +381,37 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActiv hideMenu() mainNavController.navigateSafe(R.id.action_global_ud_profile) } + + menuShareText?.setOnSingleClickListener { + hideMenu() + sendInvitation() + } + } + + private fun sendInvitation() { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra( + Intent.EXTRA_TEXT, + getString(R.string.share_invitation_message, preferences.name) + ) + type = "text/plain" + } + + val title: String = getString(R.string.share_chooser_title) + val chooser: Intent = Intent.createChooser(sendIntent, title) + + if (sendIntent.resolveActivity(packageManager) != null) { + startActivity(chooser) + } else { + showCustomToast( + ToastUI.create( + body = getString(R.string.share_no_activity_error), + leftIcon = R.drawable.ic_alert, + iconTint = R.color.accent_danger + ) + ) + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -375,6 +426,17 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActiv } private fun observeUI() { + mainViewModel.areComponentsInitialized.observe(this) { ready -> + enableUi(ready) + + if (ready) { + // LIFO ordering. + intentQueue.removeLastOrNull()?.run { + handleIntent(this) + } + } + } + contactsViewModel.showToast.onEach { toast -> toast?.let { showCustomToast(toast) @@ -404,6 +466,11 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActiv } } + private fun enableUi(enabled: Boolean) { + initializingBackground?.setVisibility(!enabled) + initializingProgressBar?.setVisibility(!enabled) + } + private fun dismissNetworkStatusMessage() { for (status in cachedNetworkStatus) { dismissIndefiniteToast(status) @@ -501,7 +568,7 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActiv Timber.v("New request sent - UI - $result") when (result) { is SimpleRequestState.Success -> { - createSnackMessage("One of your contact requests was successfully sent!") + createSnackMessage("One of your connection requests was successfully sent!") contactsViewModel.newAuthRequestSent.postValue(SimpleRequestState.Completed()) } is SimpleRequestState.Error -> { @@ -531,7 +598,7 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActiv contactsViewModel.newIncomingRequestReceived.observe(this, Observer { result -> Timber.v("New incoming request - UI - $result") if (result is SimpleRequestState.Success) { - createSnackMessage("Private channel invitation received!") + createSnackMessage("New connection request received!") contactsViewModel.newIncomingRequestReceived.postValue(SimpleRequestState.Completed()) } else { Timber.v("Completed new incoming contact") @@ -542,7 +609,7 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActiv Timber.v("New confirmation request - UI - $result") if (result is SimpleRequestState.Success) { Timber.v("Request is success") - createSnackMessage("A contact has accepted your private channel request!") + createSnackMessage("A connection has accepted your request!") contactsViewModel.newConfirmationRequestReceived.postValue(SimpleRequestState.Completed()) } else { Timber.v("Completed confirm contact post") @@ -565,7 +632,7 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActiv Timber.v("New Group Request - UI - $result") if (result is SimpleRequestState.Success) { Timber.v("Request is success") - createSnackMessage("Private Group invitation received!") + createSnackMessage("New group invitation received!") mainViewModel.newGroup.postValue(SimpleRequestState.Completed()) } else { Timber.v("Completed confirm contact post") @@ -781,10 +848,11 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActiv } companion object : BaseInstance { - const val INTENT_DEEP_LINK_BUNDLE = "nav_bundle" + const val INTENT_NOTIFICATION_CLICK = "nav_bundle" const val INTENT_PRIVATE_CHAT = "private_message" const val INTENT_GROUP_CHAT = "group_message" const val INTENT_REQUEST = "request" + const val INTENT_INVITATION = "invitation" private var activeInstances = 0 override fun activeInstancesCount(): Int { diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/MainViewModel.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/MainViewModel.kt index 52cfd7bd4eb42c35d195a64402e3fc7c7cc5046d..92ea8eafc1c8231eef71e7224ee979cf32675128 100644 --- a/app/src/main/java/io/xxlabs/messenger/ui/main/MainViewModel.kt +++ b/app/src/main/java/io/xxlabs/messenger/ui/main/MainViewModel.kt @@ -1,6 +1,7 @@ package io.xxlabs.messenger.ui.main import android.content.Context +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -54,6 +55,9 @@ class MainViewModel @Inject constructor( private var isLoggingIn: Boolean = false private var hasManagerStarted: Boolean = false + val areComponentsInitialized: LiveData<Boolean> by ::_areComponentsInitialized + private val _areComponentsInitialized = MutableLiveData(false) + @Volatile var wasLoggedIn = false @@ -331,6 +335,7 @@ class MainViewModel @Inject constructor( wasLoggedIn = true enableDummyTraffic(preferences.isCoverTrafficOn) loginProcess.postValue(DataRequestState.Success(true)) + _areComponentsInitialized.value = true } else { loginProcess.postValue(DataRequestState.Error(err)) } diff --git a/app/src/main/res/drawable/ic_connections.xml b/app/src/main/res/drawable/ic_connections.xml new file mode 100644 index 0000000000000000000000000000000000000000..4079f3f8406a42717da41af33e0947cad5bb0416 --- /dev/null +++ b/app/src/main/res/drawable/ic_connections.xml @@ -0,0 +1,31 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M6,3L18,3A1,1 0,0 1,19 4L19,20A1,1 0,0 1,18 21L6,21A1,1 0,0 1,5 20L5,4A1,1 0,0 1,6 3z" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#B1B5BA"/> + <path + android:pathData="M15,17C15,15.343 13.657,14 12,14C10.343,14 9,15.343 9,17" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#B1B5BA"/> + <path + android:pathData="M12,10m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#B1B5BA"/> + <path + android:pathData="M3,9L7,9" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#B1B5BA"/> + <path + android:pathData="M3,15L7,15" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#B1B5BA"/> +</vector> diff --git a/app/src/main/res/drawable/ic_dashboard_24px.xml b/app/src/main/res/drawable/ic_dashboard_24px.xml new file mode 100644 index 0000000000000000000000000000000000000000..87968f4aa1dea8e80982856211d6a23aa52b37f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_24px.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="18dp" + android:height="18dp" + android:viewportWidth="18" + android:viewportHeight="18"> + <path + android:pathData="M0,0H8V10H0V0ZM18,0H10V6H18V0ZM6,8V2H2V8H6ZM16,4V2H12V4H16ZM16,10V16H12V10H16ZM6,16V14H2V16H6ZM18,8H10V18H18V8ZM0,12H8V18H0V12Z" + android:fillColor="#B1B5BA" + android:fillType="evenOdd"/> +</vector> diff --git a/app/src/main/res/drawable/ic_qr_squared.xml b/app/src/main/res/drawable/ic_qr_squared.xml index 75c7ec109b1aac8b4287d00c5bd51d9ca373966d..ab0db05f5756950739c6d64c106738efc4cec863 100644 --- a/app/src/main/res/drawable/ic_qr_squared.xml +++ b/app/src/main/res/drawable/ic_qr_squared.xml @@ -5,9 +5,9 @@ android:viewportHeight="40"> <path android:pathData="M8,0L32,0A8,8 0,0 1,40 8L40,32A8,8 0,0 1,32 40L8,40A8,8 0,0 1,0 32L0,8A8,8 0,0 1,8 0z" - android:fillColor="@color/neutral_body"/> + android:fillColor="@color/neutral_active"/> <path android:pathData="M11.875,14.5C11.875,13.1227 12.9977,12 14.375,12H16.625V14H14.375C14.1023,14 13.875,14.2273 13.875,14.5V16.75H11.875V14.5ZM25.625,14H23.375V12H25.625C27.0023,12 28.125,13.1227 28.125,14.5V16.75H26.125V14.5C26.125,14.2273 25.8977,14 25.625,14ZM13.875,25.75V23.5H11.875V25.75C11.875,27.1273 12.9977,28.25 14.375,28.25H16.625V26.25H14.375C14.1023,26.25 13.875,26.0227 13.875,25.75ZM28.125,23.5V25.75C28.125,27.1273 27.0023,28.25 25.625,28.25H23.375V26.25H25.625C25.8977,26.25 26.125,26.0227 26.125,25.75V23.5H28.125ZM11,21.125H29V19.125H11V21.125Z" - android:fillColor="#242424" + android:fillColor="@color/neutral_white" android:fillType="evenOdd"/> </vector> diff --git a/app/src/main/res/drawable/ic_requests.xml b/app/src/main/res/drawable/ic_requests.xml new file mode 100644 index 0000000000000000000000000000000000000000..4d9fbd5eb27bb5a6c590ff2c16a8db26f0228113 --- /dev/null +++ b/app/src/main/res/drawable/ic_requests.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M19.2,19.4H4.8C3.806,19.4 3,18.594 3,17.6V6.722C3.042,5.758 3.836,4.999 4.8,5H19.2C20.194,5 21,5.806 21,6.8V17.6C21,18.594 20.194,19.4 19.2,19.4ZM4.8,8.481V17.6H19.2V8.481L12,13.28L4.8,8.481ZM5.52,6.8L12,11.12L18.48,6.8H5.52Z" + android:fillColor="#B1B5BA"/> +</vector> diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000000000000000000000000000000000000..098c40b4d863e371b67af6a7edc774b729301eb6 --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M17.6,18.199V15.498C17.6,15.001 18.003,14.598 18.5,14.598C18.997,14.598 19.4,15.001 19.4,15.498V18.199C19.4,19.192 18.594,20 17.604,20H6.796C5.803,20 5,19.196 5,18.199V15.498C5,15.001 5.403,14.598 5.9,14.598C6.397,14.598 6.8,15.001 6.8,15.498L6.796,18.199L17.6,18.199Z" + android:fillColor="#B1B5BA"/> + <path + android:pathData="M14.109,8.079L13.103,7.072L13.103,15.49C13.103,15.987 12.703,16.391 12.203,16.391C11.706,16.391 11.303,15.988 11.303,15.49L11.303,7.072L10.296,8.079C9.943,8.432 9.375,8.436 9.021,8.081C8.669,7.73 8.671,7.158 9.023,6.805L11.564,4.264C11.742,4.086 11.972,3.999 12.202,3.999C12.433,3.997 12.663,4.085 12.839,4.262C12.841,4.263 15.382,6.805 15.382,6.805C15.735,7.159 15.739,7.727 15.385,8.081C15.033,8.433 14.461,8.431 14.109,8.079Z" + android:fillColor="#B1B5BA"/> +</vector> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 07c988ad34041be051eca8df632d56f40cfc0728..31c1f06674d906188a6e36b547de9b0e8ea79ed0 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/mainLayout" @@ -10,12 +11,21 @@ <include android:id="@+id/mainMenuView" layout="@layout/component_menu" - android:visibility="gone" /> + android:visibility="gone" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> <androidx.coordinatorlayout.widget.CoordinatorLayout - android:layout_width="match_parent" - android:layout_height="match_parent" + android:id="@+id/contentCoordinatorLayout" + android:layout_width="0dp" + android:layout_height="0dp" android:focusable="true" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" tools:context=".ui.main.MainActivity"> <com.google.android.material.card.MaterialCardView @@ -46,13 +56,44 @@ </com.google.android.material.card.MaterialCardView> </androidx.coordinatorlayout.widget.CoordinatorLayout> + <View + android:id="@+id/initializingBackground" + android:layout_width="0dp" + android:layout_height="0dp" + android:visibility="visible" + android:background="@drawable/bg_splash_screen" + android:clickable="true" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + tools:visibility="visible"/> + + <ProgressBar + android:id="@+id/initializingProgressBar" + style="@style/XxProgressBarCircularBlue" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:indeterminate="true" + android:indeterminateTint="@color/neutral_off_white" + android:visibility="visible" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintVertical_bias="0.80" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + <ImageView android:id="@+id/mainBlurryImg" - android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_width="0dp" + android:layout_height="0dp" android:src="@drawable/bg_white" android:translationZ="@dimen/spacing_10" - android:visibility="gone" /> + android:visibility="gone" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/mainReportBtn" @@ -64,5 +105,7 @@ android:layout_marginBottom="@dimen/spacing_30" android:src="@drawable/ic_bug_report" android:visibility="gone" - android:tint="@color/white" /> -</RelativeLayout> \ No newline at end of file + android:tint="@color/white" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent"/> +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/component_menu.xml b/app/src/main/res/layout/component_menu.xml index 6f964e4af1d690820d6f09f7bbc1fb3bc02b62e6..96d721f939ee86793e1652dd54f06143ae2928cf 100644 --- a/app/src/main/res/layout/component_menu.xml +++ b/app/src/main/res/layout/component_menu.xml @@ -129,8 +129,7 @@ app:layout_constraintEnd_toEndOf="@id/menuGuideEnd" app:layout_constraintHorizontal_bias="1" app:layout_constraintStart_toEndOf="@id/menuBarrierUsername" - app:layout_constraintTop_toTopOf="@id/menuTopText" - app:layout_constraintVertical_bias="0"/> + app:layout_constraintTop_toTopOf="@id/menuTopText" /> <!-- CHATS --> <ImageView @@ -163,7 +162,7 @@ <!-- CONTACTS --> <ImageView android:id="@+id/menuContactsIcon" - android:src="@drawable/ic_menu_contacts" + android:src="@drawable/ic_connections" app:layout_constraintStart_toEndOf="@id/menuGuideStart" app:layout_constraintTop_toBottomOf="@id/menuChatsText" style="@style/menu_icon" /> @@ -188,7 +187,7 @@ <!-- REQUESTS --> <ImageView android:id="@+id/menuContactRequestsIcon" - android:src="@drawable/ic_mail" + android:src="@drawable/ic_requests" app:layout_constraintStart_toEndOf="@id/menuGuideStart" app:layout_constraintTop_toBottomOf="@id/menuContactsTxt" style="@style/menu_icon" /> @@ -262,11 +261,12 @@ <!-- SCAN --> <!-- SETTINGS --> + <ImageView android:id="@+id/menuSettingsIcon" android:src="@drawable/ic_menu_settings" app:layout_constraintStart_toEndOf="@id/menuGuideStart" - app:layout_constraintTop_toBottomOf="@id/menuJoinXxTxt" + app:layout_constraintTop_toBottomOf="@id/menuScanText" style="@style/menu_icon" /> <TextView @@ -290,9 +290,9 @@ android:layout_height="@dimen/spacing_24" android:layout_marginTop="@dimen/spacing_24" android:padding="@dimen/spacing_3" - android:src="@drawable/ic_dashboard" + android:src="@drawable/ic_dashboard_24px" app:layout_constraintStart_toEndOf="@id/menuGuideStart" - app:layout_constraintTop_toBottomOf="@id/menuScanText" + app:layout_constraintTop_toBottomOf="@id/menuSettingsTxt" style="@style/menu_icon" /> <TextView @@ -336,6 +336,31 @@ app:layout_constraintStart_toEndOf="@id/menuJoinXxIcon" app:layout_constraintTop_toTopOf="@id/menuJoinXxIcon" /> + <ImageView + android:id="@+id/menuShareIcon" + android:layout_width="@dimen/spacing_24" + android:layout_height="@dimen/spacing_24" + android:layout_marginTop="@dimen/spacing_24" + android:src="@drawable/ic_share" + app:layout_constraintStart_toEndOf="@id/menuGuideStart" + app:layout_constraintTop_toBottomOf="@id/menuJoinXxTxt" + style="@style/menu_icon" /> + + <TextView + android:id="@+id/menuShareText" + style="@style/XxTextStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_10" + android:contentDescription="menu.option.dashboard" + android:padding="@dimen/spacing_8" + android:text="Share my profile" + android:textColor="@color/neutral_weak" + android:textSize="@dimen/text_14" + app:layout_constraintBottom_toBottomOf="@id/menuShareIcon" + app:layout_constraintStart_toEndOf="@id/menuShareIcon" + app:layout_constraintTop_toTopOf="@id/menuShareIcon" /> + <TextView android:id="@+id/menuBottomBuildTxt" style="@style/XxTextStyle.SemiBold" @@ -348,7 +373,7 @@ android:textSize="@dimen/text_12" app:layout_constraintBottom_toTopOf="@id/menuBottomVersionTxt" app:layout_constraintStart_toEndOf="@id/menuGuideStart" - app:layout_constraintTop_toBottomOf="@id/menuSettingsTxt" + app:layout_constraintTop_toBottomOf="@id/menuShareText" app:layout_constraintVertical_bias="1" app:layout_constraintVertical_chainStyle="packed" tools:text="Build 1.1.0" /> diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index 9953388f02e75a5d00c5d7fc4c878c425b4de5c6..b5e8db61fb5d600fffb25c7fa669dfd0c73cd4c4 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -320,6 +320,11 @@ app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" /> + <argument + android:name="username" + app:argType="string" + app:nullable="true" + android:defaultValue="@null" /> </fragment> <fragment @@ -354,6 +359,15 @@ android:label="UdProfileFragment" tools:layout="@layout/fragment_ud_profile" /> + <action + android:id="@+id/action_global_connection_invitation" + app:destination="@id/udPrivateSearchFragment" + app:enterAnim="@anim/slide_in_right" + app:exitAnim="@anim/slide_out_left" + app:popEnterAnim="@anim/slide_in_left" + app:popExitAnim="@anim/slide_out_right" /> + + <action android:id="@+id/action_global_contact_invitation" app:destination="@id/contactInvitationFragment" diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index da985b1aff4d14d0b2b483422863680f162eb439..83d57dc0600cbb803c727d77758629f3a9d1dd46 100755 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -194,5 +194,5 @@ <color name="accent_success">#2CC069</color> <color name="accent_safe">#FE7751</color> - <color name="menu_background">#252525</color> + <color name="menu_background">@color/neutral_dark</color> </resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 35ff17573ac4a07cf783ed733c1d039a9ccd4427..7a8bab9d42bf236571ab91bf2b88acadba3759e3 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -633,4 +633,15 @@ <string name="sftp_login_password_hint">Password</string> <string name="sftp_login_port_hint">Port (leave blank for default TCP 22)</string> <string name="registration_restore_disabled_error">A username has been submitted. Please reinstall to use restore.</string> + + <string name="share_profile_label">Share profile</string> + <string name="share_invitation_message"> + Hi, I\'m using xx messenger, you can download it here: + \nhttps://invite.xx.network + \n\nAnd you can add me here: + \nhttps://elixxir.io/connect?username=%s + </string> + <string name="share_chooser_title">@string/share_profile_label</string> + <string name="share_no_activity_error">There are no apps installed to share to.</string> + <string name="search_generic_error_message">Couldn\'t complete search. Please try again.</string> </resources> \ No newline at end of file