From d59aaa761141ab38f15203cb1df21e9d46a45071 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 21 Sep 2021 16:57:55 +0200 Subject: [PATCH 1/6] 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> = 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): List { + 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() { + + @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(R.id.itemTitle) + val avatarImageView by bind(R.id.itemAvatar) + val checkedImageView by bind(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() private val directoryUsersSearch = BehaviorRelay.create() + private val identityServerUsersSearch = BehaviorRelay.create() @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? = null, val knownUsers: Async> = Uninitialized, val directoryUsers: Async> = Uninitialized, + val matchingEmail: Async = Uninitialized, val filteredMappedContacts: List = emptyList(), val pendingSelections: Set = 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" /> - + + + + + + + + + app:title="@string/invite_by_mxid_or_mail" /> + + + + + + + + + + + + + + + + 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 @@ View the room directory Name or ID (#example:matrix.org) + Search by name or ID + Search by name, ID or mail Search Name @@ -3460,6 +3462,7 @@ It’s just you at the moment. %s will be even better with others. Invite by email Invite by username + Invite by username or mail Share link Invite to %s "They’ll be able to explore %s" @@ -3476,6 +3479,13 @@ You are not currently using an identity server. In order to invite teammates and be discoverable by them, configure one below. + + Finish setting up discovery. + Invite by email, find contacts and more… + Finish setup + Discovery (%s) + + You’re not in any rooms yet. Below are some suggested rooms, but you can see more with the green button bottom right. Welcome to %1$s, %2$s. From 5bb3e228680cd16d535b0dc70e9284fcdf70c6f0 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 21 Sep 2021 17:09:28 +0200 Subject: [PATCH 2/6] cleaning --- .../im/vector/app/features/userdirectory/UserListFragment.kt | 4 ---- .../im/vector/app/features/userdirectory/UserListViewModel.kt | 1 - 2 files changed, 5 deletions(-) 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 9150511c15..5255faa524 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 @@ -67,10 +67,6 @@ 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) 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 8c20fc35b3..8cf8437183 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 @@ -47,7 +47,6 @@ import java.util.concurrent.TimeUnit private typealias KnownUsersSearch = String private typealias DirectoryUsersSearch = String -private typealias IdentityServerUserSearch = String data class ThreePidUser( val email: String, From 91ba17f71bfab66ad4f400ff9bba815e78557f98 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 22 Sep 2021 10:43:39 +0200 Subject: [PATCH 3/6] Code review --- .../app/features/userdirectory/UserListViewModel.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) 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 8cf8437183..dd045f5d32 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 @@ -101,9 +101,7 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User } private fun cleanISURL(url: String?): String? { - return if (url?.startsWith("https://") == true) { - url.substring("https://".length) - } else url + return url?.removePrefix("https://") } override fun onCleared() { @@ -186,15 +184,13 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User 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 = User(foundThreePid.matrixId))) } - } ?: Single.just(ThreePidUser(email = search, user = null).toOptional()) + } ?: Single.just(ThreePidUser(email = search, user = null)) } - .map { it.getOrNull() } - stream.toAsync { copy(matchingEmail = it) } From 0acf90d8cdd426a09ff6e22da379d5c42793749e Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 24 Sep 2021 18:11:28 +0200 Subject: [PATCH 4/6] Code review --- .../identity/DefaultIdentityService.kt | 2 + .../java/im/vector/app/core/utils/Dialogs.kt | 12 +++ .../contactsbook/ContactsBookFragment.kt | 14 +--- .../discovery/DiscoverySettingsFragment.kt | 12 +-- .../discovery/DiscoverySettingsViewModel.kt | 2 +- ...ndThreePidItem.kt => InviteByEmailItem.kt} | 2 +- .../userdirectory/UserListController.kt | 73 ++++++++++--------- .../userdirectory/UserListFragment.kt | 13 +--- .../main/res/layout/item_invite_by_mail.xml | 6 +- vector/src/main/res/values/strings.xml | 1 + 10 files changed, 68 insertions(+), 69 deletions(-) rename vector/src/main/java/im/vector/app/features/userdirectory/{FoundThreePidItem.kt => InviteByEmailItem.kt} (96%) 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 a4ad48038f..4992081c8e 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 @@ -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) diff --git a/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt index 7806f2603d..137c38a2c0 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt index 9291352821..19024fcf8b 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt @@ -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) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt index 0b8674ec6f..41b83c627d 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt @@ -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)) diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt index 11fd796534..3cd6c31ab2 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt @@ -65,7 +65,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( setState { copy( identityServer = Success(identityServerUrl), - userConsent = false + userConsent = identityService.getUserConsent() ) } if (currentIS != identityServerUrl) retrieveBinding() diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/FoundThreePidItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt similarity index 96% rename from vector/src/main/java/im/vector/app/features/userdirectory/FoundThreePidItem.kt rename to vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt index ff61f76d58..2258239bde 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/FoundThreePidItem.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt @@ -28,7 +28,7 @@ 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() { +abstract class InviteByEmailItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var foundItem: ThreePidUser 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 fbdaa22c9b..ba740f8556 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 @@ -93,45 +93,46 @@ 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 + 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 } } - 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)) + if (threePidUser.user == null) { + inviteByEmailItem { + id("email_${threePidUser.email}") + foundItem(threePidUser) + selected(isSelected) + clickListener { + host.callback?.onThreePidClick(ThreePid.Email(threePidUser.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) + } 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) + } } } } @@ -191,7 +192,7 @@ class UserListController @Inject constructor(private val session: Session, } is Loading -> { userListHeaderItem { - id("is_matching") + id("identity_server_result_header_loading") header(host.stringProvider.getString(R.string.discovery_section, currentState.configuredIdentityServer ?: "")) } loadingItem { 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 5255faa524..f251a672b8 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,7 +31,6 @@ 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 @@ -40,6 +39,7 @@ 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 @@ -228,14 +228,9 @@ class UserListFragment @Inject constructor( 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() + requireContext().showIdentityServerConsentDialog(state.configuredIdentityServer) { + viewModel.handle(UserListAction.UpdateUserConsent(true)) + } } } diff --git a/vector/src/main/res/layout/item_invite_by_mail.xml b/vector/src/main/res/layout/item_invite_by_mail.xml index 82336d2003..1cd9e105cc 100644 --- a/vector/src/main/res/layout/item_invite_by_mail.xml +++ b/vector/src/main/res/layout/item_invite_by_mail.xml @@ -1,6 +1,4 @@ - - diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index cc8ada06b6..315ac1515e 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3483,6 +3483,7 @@ Finish setting up discovery. Invite by email, find contacts and more… Finish setup + Discovery (%s) From 8318f2d38fb882a9b3249fb564acb69de1920154 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 24 Sep 2021 18:44:04 +0200 Subject: [PATCH 5/6] fix CI --- vector/src/main/java/im/vector/app/core/utils/Dialogs.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt index 137c38a2c0..c73fa70388 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt @@ -45,7 +45,7 @@ fun Context.showIdentityServerConsentDialog(configuredIdentityServer: String?, c .setTitle(R.string.identity_server_consent_dialog_title) .setMessage(getString(R.string.identity_server_consent_dialog_content, configuredIdentityServer ?: "")) .setPositiveButton(R.string.yes) { _, _ -> - consentCallBack?.invoke() + consentCallBack.invoke() } .setNegativeButton(R.string.no, null) .show() From 1fed27961a5a5a6ae7fde2543f1182c59e120fbe Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 24 Sep 2021 19:35:49 +0200 Subject: [PATCH 6/6] Code review --- .../session/identity/DefaultIdentityService.kt | 2 -- .../app/features/userdirectory/UserListViewModel.kt | 12 +++--------- 2 files changed, 3 insertions(+), 11 deletions(-) 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 4992081c8e..acd163450c 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 @@ -229,8 +229,6 @@ 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): List { 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 dd045f5d32..dead957795 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,7 +16,6 @@ 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 @@ -32,7 +31,6 @@ 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 @@ -42,7 +40,6 @@ 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 @@ -121,12 +118,9 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User } 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) - } + session.identityService().setUserConsent(action.consent) + withState { + identityServerUsersSearch.accept(it.searchTerm) } }