Merge pull request #4051 from vector-im/feature/bca/invite_user_by_mail
Support entering mail in user invite screen
This commit is contained in:
commit
9a30da13b5
1
changelog.d/4042.bugfix
Normal file
1
changelog.d/4042.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Private space invite bottomsheet only offering inviting by username not by email
|
@ -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 {
|
||||
|
@ -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
|
||||
@ -202,6 +202,8 @@ internal class DefaultIdentityService @Inject constructor(
|
||||
|
||||
identityStore.setUrl(urlCandidate)
|
||||
identityStore.setToken(token)
|
||||
// could we remember if it was previously given?
|
||||
identityStore.setUserConsent(false)
|
||||
updateIdentityAPI(urlCandidate)
|
||||
|
||||
updateAccountData(urlCandidate)
|
||||
@ -230,6 +232,8 @@ internal class DefaultIdentityService @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun lookUp(threePids: List<ThreePid>): List<FoundThreePid> {
|
||||
if (getCurrentIdentityServerUrl() == null) throw IdentityServiceError.NoIdentityServerConfigured
|
||||
|
||||
if (!getUserConsent()) {
|
||||
throw IdentityServiceError.UserConsentNotProvided
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
|
||||
/**
|
||||
* Open a web view above the current activity.
|
||||
@ -38,3 +39,14 @@ fun Context.displayInWebView(url: String) {
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
fun Context.showIdentityServerConsentDialog(configuredIdentityServer: String?, consentCallBack: (() -> Unit)) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.identity_server_consent_dialog_title)
|
||||
.setMessage(getString(R.string.identity_server_consent_dialog_content, configuredIdentityServer ?: ""))
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
consentCallBack.invoke()
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
|
@ -23,14 +23,13 @@ import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.jakewharton.rxbinding3.widget.checkedChanges
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.cleanup
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.showIdentityServerConsentDialog
|
||||
import im.vector.app.databinding.FragmentContactsBookBinding
|
||||
import im.vector.app.features.userdirectory.PendingSelection
|
||||
import im.vector.app.features.userdirectory.UserListAction
|
||||
@ -76,14 +75,9 @@ class ContactsBookFragment @Inject constructor(
|
||||
private fun setupConsentView() {
|
||||
views.phoneBookSearchForMatrixContacts.setOnClickListener {
|
||||
withState(contactsBookViewModel) { state ->
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.identity_server_consent_dialog_title)
|
||||
.setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServerUrl ?: ""))
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted)
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
requireContext().showIdentityServerConsentDialog(state.identityServerUrl) {
|
||||
contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import im.vector.app.core.extensions.observeEvent
|
||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.ensureProtocol
|
||||
import im.vector.app.core.utils.showIdentityServerConsentDialog
|
||||
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
||||
import im.vector.app.features.discovery.change.SetIdentityServerFragment
|
||||
import im.vector.app.features.settings.VectorSettingsActivity
|
||||
@ -179,14 +180,9 @@ class DiscoverySettingsFragment @Inject constructor(
|
||||
override fun onTapUpdateUserConsent(newValue: Boolean) {
|
||||
if (newValue) {
|
||||
withState(viewModel) { state ->
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.identity_server_consent_dialog_title)
|
||||
.setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServer.invoke()))
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true))
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
requireContext().showIdentityServerConsentDialog(state.identityServer.invoke()) {
|
||||
viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(false))
|
||||
|
@ -65,7 +65,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||
setState {
|
||||
copy(
|
||||
identityServer = Success(identityServerUrl),
|
||||
userConsent = false
|
||||
userConsent = identityService.getUserConsent()
|
||||
)
|
||||
}
|
||||
if (currentIS != identityServerUrl) retrieveBinding()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 InviteByEmailItem : VectorEpoxyModel<InviteByEmailItem.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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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,119 @@ class UserListController @Inject constructor(private val session: Session,
|
||||
}
|
||||
}
|
||||
|
||||
when (val matchingEmail = currentState.matchingEmail) {
|
||||
is Success -> {
|
||||
matchingEmail()?.let { threePidUser ->
|
||||
userListHeaderItem {
|
||||
id("identity_server_result_header")
|
||||
header(host.stringProvider.getString(R.string.discovery_section, currentState.configuredIdentityServer ?: ""))
|
||||
}
|
||||
val isSelected = currentState.pendingSelections.any { pendingSelection ->
|
||||
when (pendingSelection) {
|
||||
is PendingSelection.ThreePidPendingSelection -> {
|
||||
when (pendingSelection.threePid) {
|
||||
is ThreePid.Email -> pendingSelection.threePid.email == threePidUser.email
|
||||
is ThreePid.Msisdn -> false
|
||||
}
|
||||
}
|
||||
is PendingSelection.UserPendingSelection -> {
|
||||
threePidUser.user != null && threePidUser.user.userId == pendingSelection.user.userId
|
||||
}
|
||||
}
|
||||
}
|
||||
if (threePidUser.user == null) {
|
||||
inviteByEmailItem {
|
||||
id("email_${threePidUser.email}")
|
||||
foundItem(threePidUser)
|
||||
selected(isSelected)
|
||||
clickListener {
|
||||
host.callback?.onThreePidClick(ThreePid.Email(threePidUser.email))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
userDirectoryUserItem {
|
||||
id(threePidUser.user.userId)
|
||||
selected(isSelected)
|
||||
matrixItem(threePidUser.user.toMatrixItem().let {
|
||||
it.copy(
|
||||
displayName = "${it.getBestName()} [${threePidUser.email}]"
|
||||
)
|
||||
})
|
||||
avatarRenderer(host.avatarRenderer)
|
||||
clickListener {
|
||||
host.callback?.onItemClick(threePidUser.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("identity_server_result_header_loading")
|
||||
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 +314,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()
|
||||
}
|
||||
}
|
||||
|
@ -39,9 +39,11 @@ import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.setupAsSearch
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.core.utils.showIdentityServerConsentDialog
|
||||
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
|
||||
@ -131,7 +133,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 +219,21 @@ 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 ->
|
||||
requireContext().showIdentityServerConsentDialog(state.configuredIdentityServer) {
|
||||
viewModel.handle(UserListAction.UpdateUserConsent(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUseQRCode() {
|
||||
view?.hideKeyboard()
|
||||
sharedActionViewModel.post(UserListSharedAction.AddByQrCode)
|
||||
|
@ -19,18 +19,22 @@ package im.vector.app.features.userdirectory
|
||||
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 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
|
||||
@ -41,12 +45,18 @@ import java.util.concurrent.TimeUnit
|
||||
private typealias KnownUsersSearch = String
|
||||
private typealias DirectoryUsersSearch = String
|
||||
|
||||
data class ThreePidUser(
|
||||
val email: String,
|
||||
val user: User?
|
||||
)
|
||||
|
||||
class UserListViewModel @AssistedInject constructor(@Assisted initialState: UserListViewState,
|
||||
private val session: Session)
|
||||
: VectorViewModel<UserListViewState, UserListAction, UserListViewEvents>(initialState) {
|
||||
|
||||
private val knownUsersSearch = BehaviorRelay.create<KnownUsersSearch>()
|
||||
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
|
||||
private val identityServerUsersSearch = BehaviorRelay.create<String>()
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
@ -64,24 +74,72 @@ 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 url?.removePrefix("https://")
|
||||
}
|
||||
|
||||
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) {
|
||||
session.identityService().setUserConsent(action.consent)
|
||||
withState {
|
||||
identityServerUsersSearch.accept(it.searchTerm)
|
||||
}
|
||||
}
|
||||
|
||||
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 +153,45 @@ 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
|
||||
)
|
||||
)
|
||||
}
|
||||
.onErrorResumeNext {
|
||||
Single.just(ThreePidUser(email = search, user = User(foundThreePid.matrixId)))
|
||||
}
|
||||
} ?: Single.just(ThreePidUser(email = search, user = null))
|
||||
}
|
||||
stream.toAsync {
|
||||
copy(matchingEmail = it)
|
||||
}
|
||||
}
|
||||
.subscribe()
|
||||
.disposeOnClear()
|
||||
|
||||
knownUsersSearch
|
||||
.throttleLast(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@ -136,14 +227,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(
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
|
78
vector/src/main/res/layout/item_invite_by_mail.xml
Normal file
78
vector/src/main/res/layout/item_invite_by_mail.xml
Normal file
@ -0,0 +1,78 @@
|
||||
<?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/iconContainer"
|
||||
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/iconContainer"
|
||||
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>
|
@ -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,14 @@
|
||||
|
||||
<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>
|
||||
<!-- %s will be replaced by the user identity server domain, e.g vector.im -->
|
||||
<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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user