From d59aaa761141ab38f15203cb1df21e9d46a45071 Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Tue, 21 Sep 2021 16:57:55 +0200
Subject: [PATCH] Support entering mail in user invite screen

---
 changelog.d/4042.bugfix                       |   1 +
 .../org/matrix/android/sdk/rx/RxSession.kt    |   5 +
 .../identity/DefaultIdentityService.kt        |  20 +--
 .../spaces/share/ShareSpaceBottomSheet.kt     |   6 -
 .../userdirectory/FoundThreePidItem.kt        |  58 ++++++++
 .../features/userdirectory/UserListAction.kt  |   1 +
 .../userdirectory/UserListController.kt       | 119 ++++++++++++++++
 .../userdirectory/UserListFragment.kt         |  28 +++-
 .../userdirectory/UserListViewModel.kt        | 128 ++++++++++++++++--
 .../userdirectory/UserListViewState.kt        |   2 +
 .../res/layout/bottom_sheet_space_invite.xml  |  18 +--
 .../main/res/layout/item_invite_by_mail.xml   |  80 +++++++++++
 vector/src/main/res/values/strings.xml        |  10 ++
 13 files changed, 440 insertions(+), 36 deletions(-)
 create mode 100644 changelog.d/4042.bugfix
 create mode 100644 vector/src/main/java/im/vector/app/features/userdirectory/FoundThreePidItem.kt
 create mode 100644 vector/src/main/res/layout/item_invite_by_mail.xml

diff --git a/changelog.d/4042.bugfix b/changelog.d/4042.bugfix
new file mode 100644
index 0000000000..4a50bc5884
--- /dev/null
+++ b/changelog.d/4042.bugfix
@@ -0,0 +1 @@
+Private space invite bottomsheet only offering inviting by username not by email
\ No newline at end of file
diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt
index 58fb760ff5..47203816b4 100644
--- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt
+++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt
@@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_S
 import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
 import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams
 import org.matrix.android.sdk.api.session.group.model.GroupSummary
+import org.matrix.android.sdk.api.session.identity.FoundThreePid
 import org.matrix.android.sdk.api.session.identity.ThreePid
 import org.matrix.android.sdk.api.session.pushers.Pusher
 import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
@@ -239,6 +240,10 @@ class RxSession(private val session: Session) {
         )
                 .distinctUntilChanged()
     }
+
+    fun lookupThreePid(threePid: ThreePid): Single<Optional<FoundThreePid>> = rxSingle {
+        session.identityService().lookUp(listOf(threePid)).firstOrNull().toOptional()
+    }
 }
 
 fun Session.rx(): RxSession {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt
index fdb6caf53f..a4ad48038f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt
@@ -20,10 +20,16 @@ import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LifecycleRegistry
 import dagger.Lazy
+import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
 import org.matrix.android.sdk.api.auth.data.SessionParams
+import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.MatrixError
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.SessionLifecycleObserver
+import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.api.session.identity.FoundThreePid
@@ -36,23 +42,17 @@ import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
 import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate
 import org.matrix.android.sdk.internal.extensions.observeNotNull
 import org.matrix.android.sdk.internal.network.RetrofitFactory
-import org.matrix.android.sdk.api.session.SessionLifecycleObserver
 import org.matrix.android.sdk.internal.session.SessionScope
 import org.matrix.android.sdk.internal.session.identity.data.IdentityStore
+import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
 import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask
 import org.matrix.android.sdk.internal.session.profile.BindThreePidsTask
 import org.matrix.android.sdk.internal.session.profile.UnbindThreePidsTask
 import org.matrix.android.sdk.internal.session.sync.model.accountdata.IdentityServerContent
-import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
-import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource
 import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask
+import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource
 import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.util.ensureProtocol
-import kotlinx.coroutines.withContext
-import okhttp3.OkHttpClient
-import org.matrix.android.sdk.api.extensions.orFalse
-import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
 import timber.log.Timber
 import javax.inject.Inject
 import javax.net.ssl.HttpsURLConnection
@@ -227,9 +227,13 @@ internal class DefaultIdentityService @Inject constructor(
 
     override fun setUserConsent(newValue: Boolean) {
         identityStore.setUserConsent(newValue)
+        // notify listeners
+        listeners.toList().forEach { tryOrNull { it.onIdentityServerChange() } }
     }
 
     override suspend fun lookUp(threePids: List<ThreePid>): List<FoundThreePid> {
+        if (getCurrentIdentityServerUrl() == null) throw IdentityServiceError.NoIdentityServerConfigured
+
         if (!getUserConsent()) {
             throw IdentityServiceError.UserConsentNotProvided
         }
diff --git a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt
index 4289af7b3b..bd69de0d95 100644
--- a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt
+++ b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt
@@ -73,7 +73,6 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetSpa
             views.descriptionText.setTextOrHide(null)
         }
 
-        views.inviteByMailButton.isVisible = false // not yet implemented
         views.inviteByLinkButton.isVisible = state.canShareLink
         views.inviteByMxidButton.isVisible = state.canInviteByMxId
     }
@@ -81,11 +80,6 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetSpa
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
-        // XXX enable back when supported
-        views.inviteByMailButton.isVisible = false
-        views.inviteByMailButton.debouncedClicks {
-        }
-
         views.inviteByMxidButton.debouncedClicks {
             viewModel.handle(ShareSpaceAction.InviteByMxId)
         }
diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/FoundThreePidItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/FoundThreePidItem.kt
new file mode 100644
index 0000000000..ff61f76d58
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/userdirectory/FoundThreePidItem.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.userdirectory
+
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.view.isVisible
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.R
+import im.vector.app.core.epoxy.ClickListener
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.features.home.AvatarRenderer
+
+@EpoxyModelClass(layout = R.layout.item_invite_by_mail)
+abstract class FoundThreePidItem : VectorEpoxyModel<FoundThreePidItem.Holder>() {
+
+    @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
+    @EpoxyAttribute lateinit var foundItem: ThreePidUser
+    @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var clickListener: ClickListener? = null
+    @EpoxyAttribute var selected: Boolean = false
+
+    override fun bind(holder: Holder) {
+        super.bind(holder)
+        holder.itemTitleText.text = foundItem.email
+        holder.checkedImageView.isVisible = false
+        holder.avatarImageView.isVisible = true
+        holder.view.setOnClickListener(clickListener)
+        if (selected) {
+            holder.checkedImageView.isVisible = true
+            holder.avatarImageView.isVisible = false
+        } else {
+            holder.checkedImageView.isVisible = false
+            holder.avatarImageView.isVisible = true
+        }
+    }
+
+    class Holder : VectorEpoxyHolder() {
+        val itemTitleText by bind<TextView>(R.id.itemTitle)
+        val avatarImageView by bind<ImageView>(R.id.itemAvatar)
+        val checkedImageView by bind<ImageView>(R.id.itemAvatarChecked)
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt
index 7835232b09..83829c1119 100644
--- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt
+++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt
@@ -24,4 +24,5 @@ sealed class UserListAction : VectorViewModelAction {
     data class AddPendingSelection(val pendingSelection: PendingSelection) : UserListAction()
     data class RemovePendingSelection(val pendingSelection: PendingSelection) : UserListAction()
     object ComputeMatrixToLinkForSharing : UserListAction()
+    data class UpdateUserConsent(val consent: Boolean) : UserListAction()
 }
diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt
index bc2ef1f694..fbdaa22c9b 100644
--- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt
+++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt
@@ -26,9 +26,13 @@ import im.vector.app.core.epoxy.errorWithRetryItem
 import im.vector.app.core.epoxy.loadingItem
 import im.vector.app.core.epoxy.noResultItem
 import im.vector.app.core.error.ErrorFormatter
+import im.vector.app.core.resources.ColorProvider
 import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.ui.list.genericPillItem
 import im.vector.app.features.home.AvatarRenderer
+import me.gujun.android.span.span
 import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.identity.IdentityServiceError
 import org.matrix.android.sdk.api.session.identity.ThreePid
 import org.matrix.android.sdk.api.session.user.model.User
 import org.matrix.android.sdk.api.util.toMatrixItem
@@ -37,6 +41,7 @@ import javax.inject.Inject
 class UserListController @Inject constructor(private val session: Session,
                                              private val avatarRenderer: AvatarRenderer,
                                              private val stringProvider: StringProvider,
+                                             private val colorProvider: ColorProvider,
                                              private val errorFormatter: ErrorFormatter) : EpoxyController() {
 
     private var state: UserListViewState? = null
@@ -86,6 +91,118 @@ class UserListController @Inject constructor(private val session: Session,
             }
         }
 
+        when (val matchingEmail = currentState.matchingEmail) {
+            is Success -> {
+                userListHeaderItem {
+                    id("is_matching")
+                    header(host.stringProvider.getString(R.string.discovery_section, currentState.configuredIdentityServer ?: ""))
+                }
+                val invoke = matchingEmail()
+                val isSelected = currentState.pendingSelections.indexOfFirst { pendingSelection ->
+                    when (pendingSelection) {
+                        is PendingSelection.ThreePidPendingSelection -> {
+                            when (pendingSelection.threePid) {
+                                is ThreePid.Email  -> pendingSelection.threePid.email == invoke?.email
+                                is ThreePid.Msisdn -> false
+                            }
+                        }
+                        is PendingSelection.UserPendingSelection     -> {
+                            invoke?.user != null && invoke.user.userId == pendingSelection.user.userId
+                        }
+                    }
+                }  != -1
+                if (invoke?.user == null) {
+                    foundThreePidItem {
+                        id("email_${invoke?.email}")
+                        foundItem(invoke!!)
+                        selected(isSelected)
+                        clickListener {
+                            host.callback?.onThreePidClick(ThreePid.Email(invoke.email))
+                        }
+                    }
+                } else {
+                    userDirectoryUserItem {
+                        id(invoke.user.userId)
+                        selected(isSelected)
+                        matrixItem(invoke.user.toMatrixItem().let {
+                            it.copy(
+                                    displayName = "${it.displayName} [${invoke.email}]"
+                            )
+                        })
+                        avatarRenderer(host.avatarRenderer)
+                        clickListener {
+                            host.callback?.onItemClick(invoke.user)
+                        }
+                    }
+                }
+            }
+            is Fail    -> {
+                when (matchingEmail.error) {
+                    is IdentityServiceError.UserConsentNotProvided     -> {
+                        genericPillItem {
+                            id("consent_not_given")
+                            text(
+                                    span {
+                                        span {
+                                            text = host.stringProvider.getString(R.string.settings_discovery_consent_notice_off)
+                                        }
+                                        +"\n"
+                                        span {
+                                            text = host.stringProvider.getString(R.string.settings_discovery_consent_action_give_consent)
+                                            textStyle = "bold"
+                                            textColor = host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)
+                                        }
+                                    }
+                            )
+                            itemClickAction {
+                                host.callback?.giveIdentityServerConsent()
+                            }
+                        }
+                    }
+                    is IdentityServiceError.NoIdentityServerConfigured -> {
+                        genericPillItem {
+                            id("no_IDS")
+                            imageRes(R.drawable.ic_info)
+                            text(
+                                    span {
+                                        span {
+                                            text = host.stringProvider.getString(R.string.finish_setting_up_discovery)
+                                            textColor = host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)
+                                        }
+                                        +"\n"
+                                        span {
+                                            text = host.stringProvider.getString(R.string.discovery_invite)
+                                            textColor = host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
+                                        }
+                                        +"\n"
+                                        span {
+                                            text = host.stringProvider.getString(R.string.finish_setup)
+                                            textStyle = "bold"
+                                            textColor = host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)
+                                        }
+                                    }
+                            )
+                            itemClickAction {
+                                host.callback?.onSetupDiscovery()
+                            }
+                        }
+                    }
+                }
+            }
+            is Loading -> {
+                userListHeaderItem {
+                    id("is_matching")
+                    header(host.stringProvider.getString(R.string.discovery_section, currentState.configuredIdentityServer ?: ""))
+                }
+                loadingItem {
+                    id("is_loading")
+                }
+            }
+            else       -> {
+                // nop
+            }
+        }
+
         when (currentState.knownUsers) {
             is Uninitialized -> renderEmptyState()
             is Loading       -> renderLoading()
@@ -196,5 +313,7 @@ class UserListController @Inject constructor(private val session: Session,
         fun onItemClick(user: User)
         fun onMatrixIdClick(matrixId: String)
         fun onThreePidClick(threePid: ThreePid)
+        fun onSetupDiscovery()
+        fun giveIdentityServerConsent()
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt
index 6e6df7a7aa..9150511c15 100644
--- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt
@@ -31,6 +31,7 @@ import com.airbnb.mvrx.args
 import com.airbnb.mvrx.fragmentViewModel
 import com.airbnb.mvrx.withState
 import com.google.android.material.chip.Chip
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.jakewharton.rxbinding3.widget.textChanges
 import im.vector.app.R
 import im.vector.app.core.extensions.cleanup
@@ -42,6 +43,7 @@ import im.vector.app.core.utils.DimensionConverter
 import im.vector.app.core.utils.startSharePlainTextIntent
 import im.vector.app.databinding.FragmentUserListBinding
 import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
+import im.vector.app.features.settings.VectorSettingsActivity
 
 import org.matrix.android.sdk.api.session.identity.ThreePid
 import org.matrix.android.sdk.api.session.user.model.User
@@ -65,6 +67,10 @@ class UserListFragment @Inject constructor(
 
     override fun getMenuRes() = args.menuResId
 
+    override fun onResume() {
+        super.onResume()
+    }
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
         sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java)
@@ -131,7 +137,7 @@ class UserListFragment @Inject constructor(
 
     private fun setupSearchView() {
         withState(viewModel) {
-            views.userListSearch.hint = getString(R.string.user_directory_search_hint)
+            views.userListSearch.hint = getString(R.string.user_directory_search_hint_2)
         }
         views.userListSearch
                 .textChanges()
@@ -217,6 +223,26 @@ class UserListFragment @Inject constructor(
         viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.ThreePidPendingSelection(threePid)))
     }
 
+    override fun onSetupDiscovery() {
+        navigator.openSettings(
+                requireContext(),
+                VectorSettingsActivity.EXTRA_DIRECT_ACCESS_DISCOVERY_SETTINGS
+        )
+    }
+
+    override fun giveIdentityServerConsent() {
+        withState(viewModel) { state ->
+            MaterialAlertDialogBuilder(requireActivity())
+                    .setTitle(R.string.identity_server_consent_dialog_title)
+                    .setMessage(getString(R.string.identity_server_consent_dialog_content, state.configuredIdentityServer ?: ""))
+                    .setPositiveButton(R.string.yes) { _, _ ->
+                        viewModel.handle(UserListAction.UpdateUserConsent(true))
+                    }
+                    .setNegativeButton(R.string.no, null)
+                    .show()
+        }
+    }
+
     override fun onUseQRCode() {
         view?.hideKeyboard()
         sharedActionViewModel.post(UserListSharedAction.AddByQrCode)
diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt
index 5d5247ec06..8c20fc35b3 100644
--- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt
@@ -16,30 +16,43 @@
 
 package im.vector.app.features.userdirectory
 
+import androidx.lifecycle.viewModelScope
 import com.airbnb.mvrx.ActivityViewModelContext
 import com.airbnb.mvrx.FragmentViewModelContext
 import com.airbnb.mvrx.MvRxViewModelFactory
+import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.ViewModelContext
 import com.jakewharton.rxrelay2.BehaviorRelay
 import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
 import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import im.vector.app.core.extensions.exhaustive
+import im.vector.app.core.extensions.isEmail
 import im.vector.app.core.extensions.toggle
 import im.vector.app.core.platform.VectorViewModel
 import io.reactivex.Single
 import io.reactivex.android.schedulers.AndroidSchedulers
+import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.MatrixPatterns
 import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.identity.IdentityServiceListener
+import org.matrix.android.sdk.api.session.identity.ThreePid
 import org.matrix.android.sdk.api.session.profile.ProfileService
 import org.matrix.android.sdk.api.session.user.model.User
 import org.matrix.android.sdk.api.util.toMatrixItem
 import org.matrix.android.sdk.api.util.toOptional
 import org.matrix.android.sdk.rx.rx
+import timber.log.Timber
 import java.util.concurrent.TimeUnit
 
 private typealias KnownUsersSearch = String
 private typealias DirectoryUsersSearch = String
+private typealias IdentityServerUserSearch = String
+
+data class ThreePidUser(
+        val email: String,
+        val user: User?
+)
 
 class UserListViewModel @AssistedInject constructor(@Assisted initialState: UserListViewState,
                                                     private val session: Session)
@@ -47,6 +60,7 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
 
     private val knownUsersSearch = BehaviorRelay.create<KnownUsersSearch>()
     private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
+    private val identityServerUsersSearch = BehaviorRelay.create<String>()
 
     @AssistedFactory
     interface Factory {
@@ -64,24 +78,77 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
         }
     }
 
+    private val identityServerListener = object : IdentityServiceListener {
+        override fun onIdentityServerChange() {
+            withState {
+                identityServerUsersSearch.accept(it.searchTerm)
+                setState {
+                    copy(
+                            configuredIdentityServer = cleanISURL(session.identityService().getCurrentIdentityServerUrl())
+                    )
+                }
+            }
+        }
+    }
+
     init {
         observeUsers()
+        setState {
+            copy(
+                    configuredIdentityServer = cleanISURL(session.identityService().getCurrentIdentityServerUrl())
+            )
+        }
+        session.identityService().addListener(identityServerListener)
+    }
+
+    private fun cleanISURL(url: String?): String? {
+        return if (url?.startsWith("https://") == true) {
+            url.substring("https://".length)
+        } else url
+    }
+
+    override fun onCleared() {
+        session.identityService().removeListener(identityServerListener)
+        super.onCleared()
     }
 
     override fun handle(action: UserListAction) {
         when (action) {
-            is UserListAction.SearchUsers -> handleSearchUsers(action.value)
-            is UserListAction.ClearSearchUsers -> handleClearSearchUsers()
-            is UserListAction.AddPendingSelection -> handleSelectUser(action)
-            is UserListAction.RemovePendingSelection -> handleRemoveSelectedUser(action)
+            is UserListAction.SearchUsers                -> handleSearchUsers(action.value)
+            is UserListAction.ClearSearchUsers           -> handleClearSearchUsers()
+            is UserListAction.AddPendingSelection        -> handleSelectUser(action)
+            is UserListAction.RemovePendingSelection     -> handleRemoveSelectedUser(action)
             UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink()
+            is UserListAction.UpdateUserConsent          -> handleISUpdateConsent(action)
         }.exhaustive
     }
 
+    private fun handleISUpdateConsent(action: UserListAction.UpdateUserConsent) {
+        viewModelScope.launch {
+            try {
+                session.identityService().setUserConsent(action.consent)
+            } catch (failure: Throwable) {
+                Timber.d("Failed to update IS consent", failure)
+            }
+        }
+    }
+
     private fun handleSearchUsers(searchTerm: String) {
         setState {
-            copy(searchTerm = searchTerm)
+            copy(
+                    searchTerm = searchTerm
+            )
         }
+        if (searchTerm.isEmail().not()) {
+            // if it's not an email reset to uninitialized
+            // because the flow won't be triggered and result would stay
+            setState {
+                copy(
+                        matchingEmail = Uninitialized
+                )
+            }
+        }
+        identityServerUsersSearch.accept(searchTerm)
         knownUsersSearch.accept(searchTerm)
         directoryUsersSearch.accept(searchTerm)
     }
@@ -95,12 +162,47 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
     private fun handleClearSearchUsers() {
         knownUsersSearch.accept("")
         directoryUsersSearch.accept("")
+        identityServerUsersSearch.accept("")
         setState {
             copy(searchTerm = "")
         }
     }
 
     private fun observeUsers() = withState { state ->
+
+        identityServerUsersSearch
+                .filter { it.isEmail() }
+                .throttleLast(300, TimeUnit.MILLISECONDS)
+                .switchMapSingle { search ->
+                    val rx = session.rx()
+                    val stream =
+                            rx.lookupThreePid(ThreePid.Email(search)).flatMap {
+                                it.getOrNull()?.let { foundThreePid ->
+                                    rx.getProfileInfo(foundThreePid.matrixId)
+                                            .map { json ->
+                                                ThreePidUser(
+                                                        email = search,
+                                                        user = User(
+                                                                userId = foundThreePid.matrixId,
+                                                                displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String,
+                                                                avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String
+                                                        )
+                                                ).toOptional()
+                                            }
+                                            .onErrorResumeNext {
+                                                Single.just(ThreePidUser(email = search, user = User(foundThreePid.matrixId)).toOptional())
+                                            }
+                                } ?: Single.just(ThreePidUser(email = search, user = null).toOptional())
+                            }
+                                    .map { it.getOrNull() }
+
+                    stream.toAsync {
+                        copy(matchingEmail = it)
+                    }
+                }
+                .subscribe()
+                .disposeOnClear()
+
         knownUsersSearch
                 .throttleLast(300, TimeUnit.MILLISECONDS)
                 .observeOn(AndroidSchedulers.mainThread())
@@ -136,14 +238,16 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
                                                 avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String
                                         ).toOptional()
                                     }
-                                    .onErrorReturn {
+                                    .onErrorResumeNext {
                                         // Profile API can be restricted and doesn't have to return result.
                                         // In this case allow inviting valid user ids.
-                                        User(
-                                                userId = search,
-                                                displayName = null,
-                                                avatarUrl = null
-                                        ).toOptional()
+                                        Single.just(
+                                                User(
+                                                        userId = search,
+                                                        displayName = null,
+                                                        avatarUrl = null
+                                                ).toOptional()
+                                        )
                                     }
 
                             Single.zip(
diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt
index f1cbbd3b9d..b66d36c5f0 100644
--- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt
@@ -27,10 +27,12 @@ data class UserListViewState(
         val excludedUserIds: Set<String>? = null,
         val knownUsers: Async<PagedList<User>> = Uninitialized,
         val directoryUsers: Async<List<User>> = Uninitialized,
+        val matchingEmail: Async<ThreePidUser?> = Uninitialized,
         val filteredMappedContacts: List<MappedContact> = emptyList(),
         val pendingSelections: Set<PendingSelection> = emptySet(),
         val searchTerm: String = "",
         val singleSelection: Boolean,
+        val configuredIdentityServer: String? = null,
         private val showInviteActions: Boolean,
         val showContactBookAction: Boolean
 ) : MvRxState {
diff --git a/vector/src/main/res/layout/bottom_sheet_space_invite.xml b/vector/src/main/res/layout/bottom_sheet_space_invite.xml
index 03893a45f9..1fa132a086 100644
--- a/vector/src/main/res/layout/bottom_sheet_space_invite.xml
+++ b/vector/src/main/res/layout/bottom_sheet_space_invite.xml
@@ -34,14 +34,14 @@
         app:layout_constraintVertical_bias="1"
         tools:text="@string/invite_people_to_your_space_desc" />
 
-    <im.vector.app.features.spaces.create.WizardButtonView
-        android:id="@+id/inviteByMailButton"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginBottom="16dp"
-        app:icon="@drawable/ic_mail"
-        app:iconTint="?vctr_content_secondary"
-        app:title="@string/invite_by_email" />
+<!--    <im.vector.app.features.spaces.create.WizardButtonView-->
+<!--        android:id="@+id/inviteByMailButton"-->
+<!--        android:layout_width="match_parent"-->
+<!--        android:layout_height="wrap_content"-->
+<!--        android:layout_marginBottom="16dp"-->
+<!--        app:icon="@drawable/ic_mail"-->
+<!--        app:iconTint="?vctr_content_secondary"-->
+<!--        app:title="@string/invite_by_email" />-->
 
     <im.vector.app.features.spaces.create.WizardButtonView
         android:id="@+id/inviteByMxidButton"
@@ -50,7 +50,7 @@
         android:layout_marginBottom="16dp"
         app:icon="@drawable/ic_add_people"
         app:iconTint="?vctr_content_secondary"
-        app:title="@string/invite_by_mxid" />
+        app:title="@string/invite_by_mxid_or_mail" />
 
     <im.vector.app.features.spaces.create.WizardButtonView
         android:id="@+id/inviteByLinkButton"
diff --git a/vector/src/main/res/layout/item_invite_by_mail.xml b/vector/src/main/res/layout/item_invite_by_mail.xml
new file mode 100644
index 0000000000..82336d2003
--- /dev/null
+++ b/vector/src/main/res/layout/item_invite_by_mail.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+
+<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:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?android:colorBackground"
+    android:foreground="?attr/selectableItemBackground"
+    android:gravity="center_vertical"
+    android:orientation="horizontal"
+    android:padding="8dp">
+
+    <FrameLayout
+        android:id="@+id/knownUserAvatarContainer"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="8dp"
+        android:background="@drawable/rounded_rect_shape_8"
+        android:backgroundTint="?colorPrimary"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <ImageView
+            android:id="@+id/itemAvatar"
+            android:layout_width="40dp"
+            android:layout_height="40dp"
+            android:importantForAccessibility="no"
+            android:padding="4dp"
+            android:src="@drawable/ic_mail"
+            android:visibility="gone"
+            app:tint="@android:color/white"
+            tools:visibility="visible" />
+
+        <ImageView
+            android:id="@+id/itemAvatarChecked"
+            android:layout_width="40dp"
+            android:layout_height="40dp"
+            android:contentDescription="@string/a11y_checked"
+            android:scaleType="centerInside"
+            android:src="@drawable/ic_material_done"
+            app:tint="@android:color/white"
+            tools:ignore="MissingPrefix" />
+    </FrameLayout>
+
+    <TextView
+        android:id="@+id/itemTitle"
+        style="@style/Widget.Vector.TextView.Subtitle"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:layout_marginStart="12dp"
+        android:ellipsize="end"
+        android:maxLines="1"
+        android:textColor="?vctr_content_primary"
+        android:textStyle="bold"
+        app:layout_constraintBottom_toTopOf="@+id/itemDescription"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.5"
+        app:layout_constraintStart_toEndOf="@+id/knownUserAvatarContainer"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="foo@example.com" />
+
+    <TextView
+        android:id="@+id/itemDescription"
+        style="@style/Widget.Vector.TextView.Body"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:maxLines="1"
+        android:text="@string/invite_by_email"
+        android:textColor="?vctr_content_secondary"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.5"
+        app:layout_constraintStart_toStartOf="@+id/itemTitle"
+        app:layout_constraintTop_toBottomOf="@+id/itemTitle" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 453b5ba432..cc8ada06b6 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -2295,7 +2295,9 @@
     <string name="room_filtering_footer_open_room_directory">View the room directory</string>
 
     <string name="room_directory_search_hint">Name or ID (#example:matrix.org)</string>
+    <!-- TO BE REMOVED -->
     <string name="user_directory_search_hint">Search by name or ID</string>
+    <string name="user_directory_search_hint_2">Search by name, ID or mail</string>
 
     <string name="search_hint_room_name">Search Name</string>
 
@@ -3460,6 +3462,7 @@
     <string name="invite_people_to_your_space_desc">It’s just you at the moment.  %s will be even better with others.</string>
     <string name="invite_by_email">Invite by email</string>
     <string name="invite_by_mxid">Invite by username</string>
+    <string name="invite_by_mxid_or_mail">Invite by username or mail</string>
     <string name="invite_by_link">Share link</string>
     <string name="invite_to_space_with_name">Invite to %s</string>
     <string name="invite_to_space_with_name_desc">"They’ll be able to explore %s"</string>
@@ -3476,6 +3479,13 @@
 
     <string name="create_space_identity_server_info_none">You are not currently using an identity server. In order to invite teammates and be discoverable by them, configure one below.</string>
 
+
+    <string name="finish_setting_up_discovery">Finish setting up discovery.</string>
+    <string name="discovery_invite">Invite by email, find contacts and more…</string>
+    <string name="finish_setup">Finish setup</string>
+    <string name="discovery_section">Discovery (%s)</string>
+
+
     <string name="suggested_rooms_pills_on_empty_text">You’re not in any rooms yet. Below are some suggested rooms, but you can see more with the green button bottom right.</string>
     <!-- First one is the space name, and the second one is user name -->
     <string name="suggested_rooms_pills_on_empty_header">Welcome to %1$s, %2$s.</string>