Support entering mail in user invite screen

This commit is contained in:
Valere 2021-09-21 16:57:55 +02:00
parent 5a8e789435
commit d59aaa7611
13 changed files with 440 additions and 36 deletions

1
changelog.d/4042.bugfix Normal file
View file

@ -0,0 +1 @@
Private space invite bottomsheet only offering inviting by username not by email

View file

@ -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 {

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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()
}
}

View file

@ -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)

View file

@ -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(

View file

@ -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 {

View file

@ -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"

View 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>

View file

@ -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">Its 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">"Theyll 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">Youre 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>