mirror of
https://github.com/element-hq/element-android
synced 2024-09-06 17:05:20 +00:00
Support entering mail in user invite screen
This commit is contained in:
parent
5a8e789435
commit
d59aaa7611
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
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 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)
|
||||
}
|
||||
}
|
|
@ -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,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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
|
80
vector/src/main/res/layout/item_invite_by_mail.xml
Normal file
80
vector/src/main/res/layout/item_invite_by_mail.xml
Normal file
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue