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.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams
|
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.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.identity.ThreePid
|
||||||
import org.matrix.android.sdk.api.session.pushers.Pusher
|
import org.matrix.android.sdk.api.session.pushers.Pusher
|
||||||
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
|
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
|
||||||
@ -239,6 +240,10 @@ class RxSession(private val session: Session) {
|
|||||||
)
|
)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun lookupThreePid(threePid: ThreePid): Single<Optional<FoundThreePid>> = rxSingle {
|
||||||
|
session.identityService().lookUp(listOf(threePid)).firstOrNull().toOptional()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Session.rx(): RxSession {
|
fun Session.rx(): RxSession {
|
||||||
|
@ -20,10 +20,16 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.LifecycleRegistry
|
import androidx.lifecycle.LifecycleRegistry
|
||||||
import dagger.Lazy
|
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.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.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.MatrixError
|
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.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
||||||
import org.matrix.android.sdk.api.session.identity.FoundThreePid
|
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.di.UnauthenticatedWithCertificate
|
||||||
import org.matrix.android.sdk.internal.extensions.observeNotNull
|
import org.matrix.android.sdk.internal.extensions.observeNotNull
|
||||||
import org.matrix.android.sdk.internal.network.RetrofitFactory
|
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.SessionScope
|
||||||
import org.matrix.android.sdk.internal.session.identity.data.IdentityStore
|
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.openid.GetOpenIdTokenTask
|
||||||
import org.matrix.android.sdk.internal.session.profile.BindThreePidsTask
|
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.profile.UnbindThreePidsTask
|
||||||
import org.matrix.android.sdk.internal.session.sync.model.accountdata.IdentityServerContent
|
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.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.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.internal.util.ensureProtocol
|
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 timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
@ -227,9 +227,13 @@ internal class DefaultIdentityService @Inject constructor(
|
|||||||
|
|
||||||
override fun setUserConsent(newValue: Boolean) {
|
override fun setUserConsent(newValue: Boolean) {
|
||||||
identityStore.setUserConsent(newValue)
|
identityStore.setUserConsent(newValue)
|
||||||
|
// notify listeners
|
||||||
|
listeners.toList().forEach { tryOrNull { it.onIdentityServerChange() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun lookUp(threePids: List<ThreePid>): List<FoundThreePid> {
|
override suspend fun lookUp(threePids: List<ThreePid>): List<FoundThreePid> {
|
||||||
|
if (getCurrentIdentityServerUrl() == null) throw IdentityServiceError.NoIdentityServerConfigured
|
||||||
|
|
||||||
if (!getUserConsent()) {
|
if (!getUserConsent()) {
|
||||||
throw IdentityServiceError.UserConsentNotProvided
|
throw IdentityServiceError.UserConsentNotProvided
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,6 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetSpa
|
|||||||
views.descriptionText.setTextOrHide(null)
|
views.descriptionText.setTextOrHide(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
views.inviteByMailButton.isVisible = false // not yet implemented
|
|
||||||
views.inviteByLinkButton.isVisible = state.canShareLink
|
views.inviteByLinkButton.isVisible = state.canShareLink
|
||||||
views.inviteByMxidButton.isVisible = state.canInviteByMxId
|
views.inviteByMxidButton.isVisible = state.canInviteByMxId
|
||||||
}
|
}
|
||||||
@ -81,11 +80,6 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetSpa
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
// XXX enable back when supported
|
|
||||||
views.inviteByMailButton.isVisible = false
|
|
||||||
views.inviteByMailButton.debouncedClicks {
|
|
||||||
}
|
|
||||||
|
|
||||||
views.inviteByMxidButton.debouncedClicks {
|
views.inviteByMxidButton.debouncedClicks {
|
||||||
viewModel.handle(ShareSpaceAction.InviteByMxId)
|
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 AddPendingSelection(val pendingSelection: PendingSelection) : UserListAction()
|
||||||
data class RemovePendingSelection(val pendingSelection: PendingSelection) : UserListAction()
|
data class RemovePendingSelection(val pendingSelection: PendingSelection) : UserListAction()
|
||||||
object ComputeMatrixToLinkForSharing : 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.loadingItem
|
||||||
import im.vector.app.core.epoxy.noResultItem
|
import im.vector.app.core.epoxy.noResultItem
|
||||||
import im.vector.app.core.error.ErrorFormatter
|
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.resources.StringProvider
|
||||||
|
import im.vector.app.core.ui.list.genericPillItem
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
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.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.identity.ThreePid
|
||||||
import org.matrix.android.sdk.api.session.user.model.User
|
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.toMatrixItem
|
||||||
@ -37,6 +41,7 @@ import javax.inject.Inject
|
|||||||
class UserListController @Inject constructor(private val session: Session,
|
class UserListController @Inject constructor(private val session: Session,
|
||||||
private val avatarRenderer: AvatarRenderer,
|
private val avatarRenderer: AvatarRenderer,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
|
private val colorProvider: ColorProvider,
|
||||||
private val errorFormatter: ErrorFormatter) : EpoxyController() {
|
private val errorFormatter: ErrorFormatter) : EpoxyController() {
|
||||||
|
|
||||||
private var state: UserListViewState? = null
|
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) {
|
when (currentState.knownUsers) {
|
||||||
is Uninitialized -> renderEmptyState()
|
is Uninitialized -> renderEmptyState()
|
||||||
is Loading -> renderLoading()
|
is Loading -> renderLoading()
|
||||||
@ -196,5 +313,7 @@ class UserListController @Inject constructor(private val session: Session,
|
|||||||
fun onItemClick(user: User)
|
fun onItemClick(user: User)
|
||||||
fun onMatrixIdClick(matrixId: String)
|
fun onMatrixIdClick(matrixId: String)
|
||||||
fun onThreePidClick(threePid: ThreePid)
|
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.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.jakewharton.rxbinding3.widget.textChanges
|
import com.jakewharton.rxbinding3.widget.textChanges
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.cleanup
|
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.core.utils.startSharePlainTextIntent
|
||||||
import im.vector.app.databinding.FragmentUserListBinding
|
import im.vector.app.databinding.FragmentUserListBinding
|
||||||
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
|
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.identity.ThreePid
|
||||||
import org.matrix.android.sdk.api.session.user.model.User
|
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 getMenuRes() = args.menuResId
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java)
|
sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java)
|
||||||
@ -131,7 +137,7 @@ class UserListFragment @Inject constructor(
|
|||||||
|
|
||||||
private fun setupSearchView() {
|
private fun setupSearchView() {
|
||||||
withState(viewModel) {
|
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
|
views.userListSearch
|
||||||
.textChanges()
|
.textChanges()
|
||||||
@ -217,6 +223,26 @@ class UserListFragment @Inject constructor(
|
|||||||
viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.ThreePidPendingSelection(threePid)))
|
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() {
|
override fun onUseQRCode() {
|
||||||
view?.hideKeyboard()
|
view?.hideKeyboard()
|
||||||
sharedActionViewModel.post(UserListSharedAction.AddByQrCode)
|
sharedActionViewModel.post(UserListSharedAction.AddByQrCode)
|
||||||
|
@ -16,30 +16,43 @@
|
|||||||
|
|
||||||
package im.vector.app.features.userdirectory
|
package im.vector.app.features.userdirectory
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.airbnb.mvrx.ActivityViewModelContext
|
import com.airbnb.mvrx.ActivityViewModelContext
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.extensions.exhaustive
|
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.extensions.toggle
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixPatterns
|
import org.matrix.android.sdk.api.MatrixPatterns
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.profile.ProfileService
|
||||||
import org.matrix.android.sdk.api.session.user.model.User
|
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.toMatrixItem
|
||||||
import org.matrix.android.sdk.api.util.toOptional
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
import org.matrix.android.sdk.rx.rx
|
import org.matrix.android.sdk.rx.rx
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
private typealias KnownUsersSearch = String
|
private typealias KnownUsersSearch = String
|
||||||
private typealias DirectoryUsersSearch = 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,
|
class UserListViewModel @AssistedInject constructor(@Assisted initialState: UserListViewState,
|
||||||
private val session: Session)
|
private val session: Session)
|
||||||
@ -47,6 +60,7 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
|
|||||||
|
|
||||||
private val knownUsersSearch = BehaviorRelay.create<KnownUsersSearch>()
|
private val knownUsersSearch = BehaviorRelay.create<KnownUsersSearch>()
|
||||||
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
|
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
|
||||||
|
private val identityServerUsersSearch = BehaviorRelay.create<String>()
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
@ -64,8 +78,38 @@ 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 {
|
init {
|
||||||
observeUsers()
|
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) {
|
override fun handle(action: UserListAction) {
|
||||||
@ -75,13 +119,36 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
|
|||||||
is UserListAction.AddPendingSelection -> handleSelectUser(action)
|
is UserListAction.AddPendingSelection -> handleSelectUser(action)
|
||||||
is UserListAction.RemovePendingSelection -> handleRemoveSelectedUser(action)
|
is UserListAction.RemovePendingSelection -> handleRemoveSelectedUser(action)
|
||||||
UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink()
|
UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink()
|
||||||
|
is UserListAction.UpdateUserConsent -> handleISUpdateConsent(action)
|
||||||
}.exhaustive
|
}.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) {
|
private fun handleSearchUsers(searchTerm: String) {
|
||||||
setState {
|
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)
|
knownUsersSearch.accept(searchTerm)
|
||||||
directoryUsersSearch.accept(searchTerm)
|
directoryUsersSearch.accept(searchTerm)
|
||||||
}
|
}
|
||||||
@ -95,12 +162,47 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
|
|||||||
private fun handleClearSearchUsers() {
|
private fun handleClearSearchUsers() {
|
||||||
knownUsersSearch.accept("")
|
knownUsersSearch.accept("")
|
||||||
directoryUsersSearch.accept("")
|
directoryUsersSearch.accept("")
|
||||||
|
identityServerUsersSearch.accept("")
|
||||||
setState {
|
setState {
|
||||||
copy(searchTerm = "")
|
copy(searchTerm = "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeUsers() = withState { state ->
|
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
|
knownUsersSearch
|
||||||
.throttleLast(300, TimeUnit.MILLISECONDS)
|
.throttleLast(300, TimeUnit.MILLISECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
@ -136,14 +238,16 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
|
|||||||
avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String
|
avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String
|
||||||
).toOptional()
|
).toOptional()
|
||||||
}
|
}
|
||||||
.onErrorReturn {
|
.onErrorResumeNext {
|
||||||
// Profile API can be restricted and doesn't have to return result.
|
// Profile API can be restricted and doesn't have to return result.
|
||||||
// In this case allow inviting valid user ids.
|
// In this case allow inviting valid user ids.
|
||||||
|
Single.just(
|
||||||
User(
|
User(
|
||||||
userId = search,
|
userId = search,
|
||||||
displayName = null,
|
displayName = null,
|
||||||
avatarUrl = null
|
avatarUrl = null
|
||||||
).toOptional()
|
).toOptional()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Single.zip(
|
Single.zip(
|
||||||
|
@ -27,10 +27,12 @@ data class UserListViewState(
|
|||||||
val excludedUserIds: Set<String>? = null,
|
val excludedUserIds: Set<String>? = null,
|
||||||
val knownUsers: Async<PagedList<User>> = Uninitialized,
|
val knownUsers: Async<PagedList<User>> = Uninitialized,
|
||||||
val directoryUsers: Async<List<User>> = Uninitialized,
|
val directoryUsers: Async<List<User>> = Uninitialized,
|
||||||
|
val matchingEmail: Async<ThreePidUser?> = Uninitialized,
|
||||||
val filteredMappedContacts: List<MappedContact> = emptyList(),
|
val filteredMappedContacts: List<MappedContact> = emptyList(),
|
||||||
val pendingSelections: Set<PendingSelection> = emptySet(),
|
val pendingSelections: Set<PendingSelection> = emptySet(),
|
||||||
val searchTerm: String = "",
|
val searchTerm: String = "",
|
||||||
val singleSelection: Boolean,
|
val singleSelection: Boolean,
|
||||||
|
val configuredIdentityServer: String? = null,
|
||||||
private val showInviteActions: Boolean,
|
private val showInviteActions: Boolean,
|
||||||
val showContactBookAction: Boolean
|
val showContactBookAction: Boolean
|
||||||
) : MvRxState {
|
) : MvRxState {
|
||||||
|
@ -34,14 +34,14 @@
|
|||||||
app:layout_constraintVertical_bias="1"
|
app:layout_constraintVertical_bias="1"
|
||||||
tools:text="@string/invite_people_to_your_space_desc" />
|
tools:text="@string/invite_people_to_your_space_desc" />
|
||||||
|
|
||||||
<im.vector.app.features.spaces.create.WizardButtonView
|
<!-- <im.vector.app.features.spaces.create.WizardButtonView-->
|
||||||
android:id="@+id/inviteByMailButton"
|
<!-- android:id="@+id/inviteByMailButton"-->
|
||||||
android:layout_width="match_parent"
|
<!-- android:layout_width="match_parent"-->
|
||||||
android:layout_height="wrap_content"
|
<!-- android:layout_height="wrap_content"-->
|
||||||
android:layout_marginBottom="16dp"
|
<!-- android:layout_marginBottom="16dp"-->
|
||||||
app:icon="@drawable/ic_mail"
|
<!-- app:icon="@drawable/ic_mail"-->
|
||||||
app:iconTint="?vctr_content_secondary"
|
<!-- app:iconTint="?vctr_content_secondary"-->
|
||||||
app:title="@string/invite_by_email" />
|
<!-- app:title="@string/invite_by_email" />-->
|
||||||
|
|
||||||
<im.vector.app.features.spaces.create.WizardButtonView
|
<im.vector.app.features.spaces.create.WizardButtonView
|
||||||
android:id="@+id/inviteByMxidButton"
|
android:id="@+id/inviteByMxidButton"
|
||||||
@ -50,7 +50,7 @@
|
|||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
app:icon="@drawable/ic_add_people"
|
app:icon="@drawable/ic_add_people"
|
||||||
app:iconTint="?vctr_content_secondary"
|
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
|
<im.vector.app.features.spaces.create.WizardButtonView
|
||||||
android:id="@+id/inviteByLinkButton"
|
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_filtering_footer_open_room_directory">View the room directory</string>
|
||||||
|
|
||||||
<string name="room_directory_search_hint">Name or ID (#example:matrix.org)</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">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>
|
<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_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_email">Invite by email</string>
|
||||||
<string name="invite_by_mxid">Invite by username</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_by_link">Share link</string>
|
||||||
<string name="invite_to_space_with_name">Invite to %s</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>
|
<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="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>
|
<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 -->
|
<!-- 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>
|
<string name="suggested_rooms_pills_on_empty_header">Welcome to %1$s, %2$s.</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user