Identity: import UI/UX From Riot and adapt to RiotX architecture

This commit is contained in:
Benoit Marty 2020-05-06 23:34:26 +02:00
parent 0199cf9a03
commit 784918350b
35 changed files with 2440 additions and 30 deletions

View File

@ -27,7 +27,7 @@ interface IdentityService {
/**
* Return the default identity server of the homeserver (using Wellknown request)
*/
fun getDefaultIdentityServer(): String?
fun getDefaultIdentityServer(callback: MatrixCallback<String?>): Cancelable
fun getCurrentIdentityServer(): String?
@ -35,9 +35,11 @@ interface IdentityService {
fun disconnect()
fun bindThreePid()
fun startBindSession(threePid: ThreePid, nothing: Nothing?, matrixCallback: MatrixCallback<ThreePid>)
fun finalizeBindSessionFor3PID(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>)
fun submitValidationToken(pid: ThreePid, code: String, matrixCallback: MatrixCallback<Unit>)
fun unbindThreePid()
fun startUnBindSession(threePid: ThreePid, nothing: Nothing?, matrixCallback: MatrixCallback<Pair<Boolean, ThreePid?>>)
fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable

View File

@ -18,5 +18,5 @@ package im.vector.matrix.android.api.session.identity
sealed class ThreePid(open val value: String) {
data class Email(val email: String) : ThreePid(email)
data class Msisdn(val msisdn: String) : ThreePid(msisdn)
data class Msisdn(val msisdn: String, val countryCode: String? = null) : ThreePid(msisdn)
}

View File

@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.identity.IdentityServiceError
import im.vector.matrix.android.api.session.identity.IdentityServiceListener
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.RetrofitFactory
@ -78,7 +79,7 @@ internal class DefaultIdentityService @Inject constructor(
lifecycleRegistry.currentState = Lifecycle.State.STARTED
// Observe the account data change
accountDataDataSource
.getLiveAccountDataEvent(UserAccountData.TYPE_IDENTITY)
.getLiveAccountDataEvent(UserAccountData.TYPE_IDENTITY_SERVER)
.observeNotNull(lifecycleOwner) {
val identityServerContent = it.getOrNull()?.content?.toModel<UserAccountDataIdentity>()
if (identityServerContent != null) {
@ -104,8 +105,10 @@ internal class DefaultIdentityService @Inject constructor(
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
}
override fun getDefaultIdentityServer(): String? {
TODO("Not yet implemented")
override fun getDefaultIdentityServer(callback: MatrixCallback<String?>): Cancelable {
// TODO Use Wellknown request
callback.onSuccess("https://vector.im")
return NoOpCancellable
}
override fun getCurrentIdentityServer(): String? {
@ -116,22 +119,49 @@ internal class DefaultIdentityService @Inject constructor(
TODO("Not yet implemented")
}
override fun startBindSession(threePid: ThreePid, nothing: Nothing?, matrixCallback: MatrixCallback<ThreePid>) {
TODO("Not yet implemented")
}
override fun finalizeBindSessionFor3PID(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>) {
TODO("Not yet implemented")
}
override fun submitValidationToken(pid: ThreePid, code: String, matrixCallback: MatrixCallback<Unit>) {
TODO("Not yet implemented")
}
override fun startUnBindSession(threePid: ThreePid, nothing: Nothing?, matrixCallback: MatrixCallback<Pair<Boolean, ThreePid?>>) {
TODO("Not yet implemented")
}
override fun setNewIdentityServer(url: String?, callback: MatrixCallback<Unit>): Cancelable {
val urlCandidate = url?.let { param ->
buildString {
if (!param.startsWith("http")) {
append("https://")
}
append(param)
}
}
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
val current = getCurrentIdentityServer()
when (url) {
when (urlCandidate) {
current ->
// Nothing to do
Timber.d("Same URL, nothing to do")
null -> {
// TODO
// Disconnect previous one if any
// TODO Disconnect previous one if any
identityServiceStore.setUrl(null)
updateAccountData(null)
}
else -> {
// TODO: check first that it is a valid identity server
updateAccountData(url)
// Try to get a token
getIdentityServerToken(urlCandidate)
updateAccountData(urlCandidate)
}
}
}
@ -143,14 +173,6 @@ internal class DefaultIdentityService @Inject constructor(
))
}
override fun bindThreePid() {
TODO("Not yet implemented")
}
override fun unbindThreePid() {
TODO("Not yet implemented")
}
override fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
lookUpInternal(true, threePids)
@ -181,15 +203,19 @@ internal class DefaultIdentityService @Inject constructor(
if (entity.token == null) {
// Try to get a token
val openIdToken = openIdTokenTask.execute(Unit)
val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken))
identityServiceStore.setToken(token.token)
getIdentityServerToken(url)
}
}
private suspend fun getIdentityServerToken(url: String) {
val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
val openIdToken = openIdTokenTask.execute(Unit)
val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken))
identityServiceStore.setToken(token.token)
}
override fun addListener(listener: IdentityServiceListener) {
listeners.add(listener)
}

View File

@ -30,6 +30,6 @@ abstract class UserAccountData : AccountDataContent {
const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls"
const val TYPE_WIDGETS = "m.widgets"
const val TYPE_PUSH_RULES = "m.push_rules"
const val TYPE_IDENTITY = "m.identity"
const val TYPE_IDENTITY_SERVER = "m.identity_server"
}
}

View File

@ -21,7 +21,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UserAccountDataIdentity(
@Json(name = "type") override val type: String = TYPE_IDENTITY,
@Json(name = "type") override val type: String = TYPE_IDENTITY_SERVER,
@Json(name = "content") val content: IdentityContent? = null
) : UserAccountData()

View File

@ -32,7 +32,7 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
fun getData(): Any
}
data class IdentityParams(override val type: String = UserAccountData.TYPE_IDENTITY,
data class IdentityParams(override val type: String = UserAccountData.TYPE_IDENTITY_SERVER,
private val identityContent: IdentityContent
) : Params {

View File

@ -22,6 +22,7 @@ import androidx.fragment.app.FragmentFactory
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import im.vector.riotx.features.discovery.DiscoverySettingsFragment
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment
@ -41,6 +42,7 @@ import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeF
import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQRWaitingFragment
import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQrScannedByOtherFragment
import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment
import im.vector.riotx.features.discovery.change.SetIdentityServerFragment
import im.vector.riotx.features.grouplist.GroupListFragment
import im.vector.riotx.features.home.HomeDetailFragment
import im.vector.riotx.features.home.HomeDrawerFragment
@ -474,4 +476,14 @@ interface FragmentModule {
@IntoMap
@FragmentKey(SharedSecuredStorageKeyFragment::class)
fun bindSharedSecuredStorageKeyFragment(fragment: SharedSecuredStorageKeyFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SetIdentityServerFragment::class)
fun bindSetIdentityServerFragment(fragment: SetIdentityServerFragment): Fragment
@Binds
@IntoMap
@FragmentKey(DiscoverySettingsFragment::class)
fun bindDiscoverySettingsFragment(fragment: DiscoverySettingsFragment): Fragment
}

View File

@ -26,6 +26,7 @@ import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromK
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.riotx.features.discovery.DiscoverySharedViewModel
import im.vector.riotx.features.home.HomeSharedActionViewModel
import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel
import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel
@ -118,4 +119,9 @@ interface ViewModelModule {
@IntoMap
@ViewModelKey(RoomProfileSharedActionViewModel::class)
fun bindRoomProfileSharedActionViewModel(viewModel: RoomProfileSharedActionViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(DiscoverySharedViewModel::class)
fun bindDiscoverySharedViewModel(viewModel: DiscoverySharedViewModel): ViewModel
}

View File

@ -0,0 +1,303 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.google.i18n.phonenumbers.PhoneNumberUtil
import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import javax.inject.Inject
class DiscoverySettingsController @Inject constructor(
private val colorProvider: ColorProvider,
private val stringProvider: StringProvider
) : TypedEpoxyController<DiscoverySettingsState>() {
var listener: Listener? = null
override fun buildModels(data: DiscoverySettingsState) {
when (data.identityServer) {
is Loading -> {
settingsLoadingItem {
id("identityServerLoading")
}
}
is Fail -> {
settingsInfoItem {
id("identityServerError")
helperText(data.identityServer.error.message)
}
}
is Success -> {
buildIdentityServerSection(data)
val hasIdentityServer = data.identityServer().isNullOrBlank().not()
if (hasIdentityServer) {
buildMailSection(data)
buildPhoneNumberSection(data)
}
}
}
}
private fun buildPhoneNumberSection(data: DiscoverySettingsState) {
settingsSectionTitle {
id("msisdn")
titleResId(R.string.settings_discovery_msisdn_title)
}
when (data.phoneNumbersList) {
is Loading -> {
settingsLoadingItem {
id("phoneLoading")
}
}
is Fail -> {
settingsInfoItem {
id("msisdnListError")
helperText(data.phoneNumbersList.error.message)
}
}
is Success -> {
val phones = data.phoneNumbersList.invoke()
if (phones.isEmpty()) {
settingsInfoItem {
id("no_msisdn")
helperText(stringProvider.getString(R.string.settings_discovery_no_msisdn))
}
} else {
phones.forEach { piState ->
val phoneNumber = PhoneNumberUtil.getInstance()
.parse("+${piState.value}", null)
?.let {
PhoneNumberUtil.getInstance().format(it, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
}
settingsTextButtonItem {
id(piState.value)
title(phoneNumber)
colorProvider(colorProvider)
stringProvider(stringProvider)
when {
piState.isShared is Loading -> buttonIndeterminate(true)
piState.isShared is Fail -> {
buttonType(SettingsTextButtonItem.ButtonType.NORMAL)
buttonStyle(SettingsTextButtonItem.ButtonStyle.DESTRUCTIVE)
buttonTitle(stringProvider.getString(R.string.global_retry))
infoMessage(piState.isShared.error.message)
buttonClickListener(View.OnClickListener {
listener?.onTapRetryToRetrieveBindings()
})
}
piState.isShared is Success -> when (piState.isShared()) {
PidInfo.SharedState.SHARED,
PidInfo.SharedState.NOT_SHARED -> {
checked(piState.isShared() == PidInfo.SharedState.SHARED)
buttonType(SettingsTextButtonItem.ButtonType.SWITCH)
switchChangeListener { _, checked ->
if (checked) {
listener?.onTapShareMsisdn(piState.value)
} else {
listener?.onTapRevokeMsisdn(piState.value)
}
}
}
PidInfo.SharedState.NOT_VERIFIED_FOR_BIND,
PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND -> {
buttonType(SettingsTextButtonItem.ButtonType.NORMAL)
buttonTitle("")
}
}
}
}
when (piState.isShared()) {
PidInfo.SharedState.NOT_VERIFIED_FOR_BIND,
PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND -> {
settingsItemText {
id("tverif" + piState.value)
descriptionText(stringProvider.getString(R.string.settings_text_message_sent, phoneNumber))
interactionListener(object : SettingsItemText.Listener {
override fun onValidate(code: String) {
val bind = piState.isShared() == PidInfo.SharedState.NOT_VERIFIED_FOR_BIND
listener?.checkMsisdnVerification(piState.value, code, bind)
}
})
}
}
else -> {
}
}
}
}
}
}
}
private fun buildMailSection(data: DiscoverySettingsState) {
settingsSectionTitle {
id("emails")
titleResId(R.string.settings_discovery_emails_title)
}
when (data.emailList) {
is Loading -> {
settingsLoadingItem {
id("mailLoading")
}
}
is Fail -> {
settingsInfoItem {
id("mailListError")
helperText(data.emailList.error.message)
}
}
is Success -> {
val emails = data.emailList.invoke()
if (emails.isEmpty()) {
settingsInfoItem {
id("no_emails")
helperText(stringProvider.getString(R.string.settings_discovery_no_mails))
}
} else {
emails.forEach { piState ->
settingsTextButtonItem {
id(piState.value)
title(piState.value)
colorProvider(colorProvider)
stringProvider(stringProvider)
when (piState.isShared) {
is Loading -> buttonIndeterminate(true)
is Fail -> {
buttonType(SettingsTextButtonItem.ButtonType.NORMAL)
buttonStyle(SettingsTextButtonItem.ButtonStyle.DESTRUCTIVE)
buttonTitle(stringProvider.getString(R.string.global_retry))
infoMessage(piState.isShared.error.message)
buttonClickListener(View.OnClickListener {
listener?.onTapRetryToRetrieveBindings()
})
}
is Success -> when (piState.isShared()) {
PidInfo.SharedState.SHARED,
PidInfo.SharedState.NOT_SHARED -> {
checked(piState.isShared() == PidInfo.SharedState.SHARED)
buttonType(SettingsTextButtonItem.ButtonType.SWITCH)
switchChangeListener { _, checked ->
if (checked) {
listener?.onTapShareEmail(piState.value)
} else {
listener?.onTapRevokeEmail(piState.value)
}
}
}
PidInfo.SharedState.NOT_VERIFIED_FOR_BIND,
PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND -> {
buttonType(SettingsTextButtonItem.ButtonType.NORMAL)
buttonTitleId(R.string._continue)
infoMessageTintColorId(R.color.vector_info_color)
infoMessage(stringProvider.getString(R.string.settings_discovery_confirm_mail, piState.value))
buttonClickListener(View.OnClickListener {
val bind = piState.isShared() == PidInfo.SharedState.NOT_VERIFIED_FOR_BIND
listener?.checkEmailVerification(piState.value, bind)
})
}
}
}
}
}
}
}
}
}
private fun buildIdentityServerSection(data: DiscoverySettingsState) {
val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none)
settingsSectionTitle {
id("idsTitle")
titleResId(R.string.identity_server)
}
settingsItem {
id("idServer")
description(identityServer)
}
settingsInfoItem {
id("idServerFooter")
if (data.termsNotSigned) {
helperText(stringProvider.getString(R.string.settings_agree_to_terms, identityServer))
showCompoundDrawable(true)
itemClickListener(View.OnClickListener { listener?.onSelectIdentityServer() })
} else {
showCompoundDrawable(false)
if (data.identityServer() != null) {
helperText(stringProvider.getString(R.string.settings_discovery_identity_server_info, identityServer))
} else {
helperTextResId(R.string.settings_discovery_identity_server_info_none)
}
}
}
settingsButtonItem {
id("change")
colorProvider(colorProvider)
if (data.identityServer() != null) {
buttonTitleId(R.string.change_identity_server)
} else {
buttonTitleId(R.string.add_identity_server)
}
buttonStyle(SettingsTextButtonItem.ButtonStyle.POSITIVE)
buttonClickListener(View.OnClickListener {
listener?.onTapChangeIdentityServer()
})
}
if (data.identityServer() != null) {
settingsInfoItem {
id("removeInfo")
helperTextResId(R.string.settings_discovery_disconnect_identity_server_info)
}
settingsButtonItem {
id("remove")
colorProvider(colorProvider)
buttonTitleId(R.string.disconnect_identity_server)
buttonStyle(SettingsTextButtonItem.ButtonStyle.DESTRUCTIVE)
buttonClickListener(View.OnClickListener {
listener?.onTapDisconnectIdentityServer()
})
}
}
}
interface Listener {
fun onSelectIdentityServer()
fun onTapRevokeEmail(email: String)
fun onTapShareEmail(email: String)
fun checkEmailVerification(email: String, bind: Boolean)
fun checkMsisdnVerification(msisdn: String, code: String, bind: Boolean)
fun onTapRevokeMsisdn(msisdn: String)
fun onTapShareMsisdn(msisdn: String)
fun onTapChangeIdentityServer()
fun onTapDisconnectIdentityServer()
fun onTapRetryToRetrieveBindings()
}
}

View File

@ -0,0 +1,199 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.Observer
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.discovery.change.SetIdentityServerFragment
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject
class DiscoverySettingsFragment @Inject constructor(
private val controller: DiscoverySettingsController,
val viewModelFactory: DiscoverySettingsViewModel.Factory
) : VectorBaseFragment(), DiscoverySettingsController.Listener {
override fun getLayoutResId() = R.layout.fragment_generic_recycler
private val viewModel by fragmentViewModel(DiscoverySettingsViewModel::class)
lateinit var sharedViewModel: DiscoverySharedViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel = activityViewModelProvider.get(DiscoverySharedViewModel::class.java)
controller.listener = this
recyclerView.configureWith(controller)
sharedViewModel.navigateEvent.observe(viewLifecycleOwner, Observer {
if (it.peekContent().first == DiscoverySharedViewModel.NEW_IDENTITY_SERVER_SET_REQUEST) {
viewModel.handle(DiscoverySettingsAction.ChangeIdentityServer(it.peekContent().second))
}
})
viewModel.observeViewEvents {
when (it) {
is DiscoverySettingsViewEvents.Failure -> {
// TODO Snackbar.make(view, throwable.toString(), Snackbar.LENGTH_LONG).show()
}
}.exhaustive
}
}
override fun onDestroyView() {
recyclerView.cleanup()
controller.listener = null
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { state ->
controller.setData(state)
}
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_discovery_category)
//If some 3pids are pending, we can try to check if they have been verified here
viewModel.handle(DiscoverySettingsAction.Refresh)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
/* TODO
if (requestCode == TERMS_REQUEST_CODE) {
if (Activity.RESULT_OK == resultCode) {
viewModel.refreshModel()
} else {
//add some error?
}
}
*/
super.onActivityResult(requestCode, resultCode, data)
}
override fun onSelectIdentityServer() = withState(viewModel) { state ->
if (state.termsNotSigned) {
/*
TODO
ReviewTermsActivity.intent(requireContext(),
TermsManager.ServiceType.IdentityService,
SetIdentityServerViewModel.sanitatizeBaseURL(state.identityServer() ?: ""),
null).also {
startActivityForResult(it, TERMS_REQUEST_CODE)
}
*/
}
}
override fun onTapRevokeEmail(email: String) {
viewModel.handle(DiscoverySettingsAction.RevokeThreePid(ThreePid.Email(email)))
}
override fun onTapShareEmail(email: String) {
viewModel.handle(DiscoverySettingsAction.ShareThreePid(ThreePid.Email(email)))
}
override fun checkEmailVerification(email: String, bind: Boolean) {
viewModel.handle(DiscoverySettingsAction.FinalizeBind3pid(ThreePid.Email(email), bind))
}
override fun checkMsisdnVerification(msisdn: String, code: String, bind: Boolean) {
viewModel.handle(DiscoverySettingsAction.SubmitMsisdnToken(msisdn, code, bind))
}
override fun onTapRevokeMsisdn(msisdn: String) {
viewModel.handle(DiscoverySettingsAction.RevokeThreePid(ThreePid.Msisdn(msisdn)))
}
override fun onTapShareMsisdn(msisdn: String) {
viewModel.handle(DiscoverySettingsAction.ShareThreePid(ThreePid.Msisdn(msisdn)))
}
override fun onTapChangeIdentityServer() = withState(viewModel) { state ->
//we should prompt if there are bound items with current is
val pidList = ArrayList<PidInfo>().apply {
state.emailList()?.let { addAll(it) }
state.phoneNumbersList()?.let { addAll(it) }
}
val hasBoundIds = pidList.any { it.isShared() == PidInfo.SharedState.SHARED }
if (hasBoundIds) {
//we should prompt
AlertDialog.Builder(requireActivity())
.setTitle(R.string.change_identity_server)
.setMessage(getString(R.string.settings_discovery_disconnect_with_bound_pid, state.identityServer(), state.identityServer()))
.setPositiveButton(R.string._continue) { _, _ -> navigateToChangeIdentityServerFragment() }
.setNegativeButton(R.string.cancel, null)
.show()
Unit
} else {
navigateToChangeIdentityServerFragment()
}
}
override fun onTapDisconnectIdentityServer() {
//we should prompt if there are bound items with current is
withState(viewModel) { state ->
val pidList = ArrayList<PidInfo>().apply {
state.emailList()?.let { addAll(it) }
state.phoneNumbersList()?.let { addAll(it) }
}
val hasBoundIds = pidList.any { it.isShared() == PidInfo.SharedState.SHARED }
if (hasBoundIds) {
//we should prompt
AlertDialog.Builder(requireActivity())
.setTitle(R.string.disconnect_identity_server)
.setMessage(getString(R.string.settings_discovery_disconnect_with_bound_pid, state.identityServer(), state.identityServer()))
.setPositiveButton(R.string._continue) { _, _ -> viewModel.handle(DiscoverySettingsAction.ChangeIdentityServer(null)) }
.setNegativeButton(R.string.cancel, null)
.show()
} else {
viewModel.handle(DiscoverySettingsAction.ChangeIdentityServer(null))
}
}
}
override fun onTapRetryToRetrieveBindings() {
viewModel.handle(DiscoverySettingsAction.RetrieveBinding)
}
private fun navigateToChangeIdentityServerFragment() {
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom, R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom)
.replace(R.id.vector_settings_page, SetIdentityServerFragment::class.java, null)
.addToBackStack(null)
.commit()
}
}

View File

@ -0,0 +1,503 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.identity.IdentityServiceListener
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
data class PidInfo(
val value: String,
val isShared: Async<SharedState>,
val _3pid: ThreePid? = null
) {
enum class SharedState {
SHARED,
NOT_SHARED,
NOT_VERIFIED_FOR_BIND,
NOT_VERIFIED_FOR_UNBIND
}
}
data class DiscoverySettingsState(
val identityServer: Async<String?> = Uninitialized,
val emailList: Async<List<PidInfo>> = Uninitialized,
val phoneNumbersList: Async<List<PidInfo>> = Uninitialized,
// TODO Use ViewEvents
val termsNotSigned: Boolean = false
) : MvRxState
sealed class DiscoverySettingsAction : VectorViewModelAction {
object RetrieveBinding : DiscoverySettingsAction()
object Refresh : DiscoverySettingsAction()
data class ChangeIdentityServer(val url: String?) : DiscoverySettingsAction()
data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
data class FinalizeBind3pid(val threePid: ThreePid, val bind: Boolean) : DiscoverySettingsAction()
data class SubmitMsisdnToken(val msisdn: String, val code: String, val bind: Boolean) : DiscoverySettingsAction()
}
sealed class DiscoverySettingsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : DiscoverySettingsViewEvents()
}
class DiscoverySettingsViewModel @AssistedInject constructor(
@Assisted initialState: DiscoverySettingsState,
private val session: Session)
: VectorViewModel<DiscoverySettingsState, DiscoverySettingsAction, DiscoverySettingsViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: DiscoverySettingsState): DiscoverySettingsViewModel
}
companion object : MvRxViewModelFactory<DiscoverySettingsViewModel, DiscoverySettingsState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: DiscoverySettingsState): DiscoverySettingsViewModel? {
val fragment: DiscoverySettingsFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewModelFactory.create(state)
}
}
private val identityService = session.identityService()
private val identityServerManagerListener = object : IdentityServiceListener {
override fun onIdentityServerChange() = withState { state ->
val identityServerUrl = identityService.getCurrentIdentityServer()
val currentIS = state.identityServer()
setState {
copy(identityServer = Success(identityServerUrl))
}
if (currentIS != identityServerUrl) refreshModel()
}
}
init {
startListenToIdentityManager()
refreshModel()
}
override fun onCleared() {
super.onCleared()
stopListenToIdentityManager()
}
override fun handle(action: DiscoverySettingsAction) {
when (action) {
DiscoverySettingsAction.Refresh -> refreshPendingEmailBindings()
DiscoverySettingsAction.RetrieveBinding -> retrieveBinding()
is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action)
is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action)
is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action)
is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action)
is DiscoverySettingsAction.SubmitMsisdnToken -> submitMsisdnToken(action)
}.exhaustive
}
private fun changeIdentityServer(action: DiscoverySettingsAction.ChangeIdentityServer) {
setState {
copy(
identityServer = Loading()
)
}
session.identityService().setNewIdentityServer(action.url, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
setState {
copy(
identityServer = Success(action.url)
)
}
refreshModel()
}
override fun onFailure(failure: Throwable) {
setState {
copy(
identityServer = Fail(failure)
)
}
}
})
}
private fun shareThreePid(action: DiscoverySettingsAction.ShareThreePid) {
when (action.threePid) {
is ThreePid.Email -> shareEmail(action.threePid.email)
is ThreePid.Msisdn -> shareMsisdn(action.threePid.msisdn)
}.exhaustive
}
private fun shareEmail(email: String) = withState { state ->
if (state.identityServer() == null) return@withState
changeMailState(email, Loading(), null)
identityService.startBindSession(ThreePid.Email(email), null,
object : MatrixCallback<ThreePid> {
override fun onSuccess(data: ThreePid) {
changeMailState(email, Success(PidInfo.SharedState.NOT_VERIFIED_FOR_BIND), data)
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
changeMailState(email, Fail(failure))
}
})
}
private fun changeMailState(address: String, state: Async<PidInfo.SharedState>, threePid: ThreePid?) {
setState {
val currentMails = emailList() ?: emptyList()
copy(emailList = Success(
currentMails.map {
if (it.value == address) {
it.copy(
_3pid = threePid,
isShared = state
)
} else {
it
}
}
))
}
}
private fun changeMailState(address: String, state: Async<PidInfo.SharedState>) {
setState {
val currentMails = emailList() ?: emptyList()
copy(emailList = Success(
currentMails.map {
if (it.value == address) {
it.copy(isShared = state)
} else {
it
}
}
))
}
}
private fun changeMsisdnState(address: String, state: Async<PidInfo.SharedState>, threePid: ThreePid?) {
setState {
val phones = phoneNumbersList() ?: emptyList()
copy(phoneNumbersList = Success(
phones.map {
if (it.value == address) {
it.copy(
_3pid = threePid,
isShared = state
)
} else {
it
}
}
))
}
}
private fun revokeThreePid(action: DiscoverySettingsAction.RevokeThreePid) {
when (action.threePid) {
is ThreePid.Email -> revokeEmail(action.threePid.email)
is ThreePid.Msisdn -> revokeMsisdn(action.threePid.msisdn)
}.exhaustive
}
private fun revokeEmail(email: String) = withState { state ->
if (state.identityServer() == null) return@withState
if (state.emailList() == null) return@withState
changeMailState(email, Loading())
identityService.startUnBindSession(ThreePid.Email(email), null, object : MatrixCallback<Pair<Boolean, ThreePid?>> {
override fun onSuccess(data: Pair<Boolean, ThreePid?>) {
if (data.first) {
// requires mail validation
changeMailState(email, Success(PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND), data.second)
} else {
changeMailState(email, Success(PidInfo.SharedState.NOT_SHARED))
}
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
changeMailState(email, Fail(failure))
}
})
}
private fun revokeMsisdn(msisdn: String) = withState { state ->
if (state.identityServer() == null) return@withState
if (state.emailList() == null) return@withState
changeMsisdnState(msisdn, Loading())
val phoneNumber = PhoneNumberUtil.getInstance()
.parse("+$msisdn", null)
val countryCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode)
identityService.startUnBindSession(ThreePid.Msisdn(msisdn, countryCode), null, object : MatrixCallback<Pair<Boolean, ThreePid?>> {
override fun onSuccess(data: Pair<Boolean, ThreePid?>) {
if (data.first /*requires mail validation */) {
changeMsisdnState(msisdn, Success(PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND), data.second)
} else {
changeMsisdnState(msisdn, Success(PidInfo.SharedState.NOT_SHARED))
}
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
changeMsisdnState(msisdn, Fail(failure))
}
})
}
private fun shareMsisdn(msisdn: String) = withState { state ->
if (state.identityServer() == null) return@withState
changeMsisdnState(msisdn, Loading())
val phoneNumber = PhoneNumberUtil.getInstance()
.parse("+$msisdn", null)
val countryCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode)
identityService.startBindSession(ThreePid.Msisdn(msisdn, countryCode), null, object : MatrixCallback<ThreePid> {
override fun onSuccess(data: ThreePid) {
changeMsisdnState(msisdn, Success(PidInfo.SharedState.NOT_VERIFIED_FOR_BIND), data)
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
changeMsisdnState(msisdn, Fail(failure))
}
})
}
private fun changeMsisdnState(msisdn: String, sharedState: Async<PidInfo.SharedState>) {
setState {
val currentMsisdns = phoneNumbersList()!!
copy(phoneNumbersList = Success(
currentMsisdns.map {
if (it.value == msisdn) {
it.copy(isShared = sharedState)
} else {
it
}
})
)
}
}
private fun startListenToIdentityManager() {
identityService.addListener(identityServerManagerListener)
}
private fun stopListenToIdentityManager() {
identityService.addListener(identityServerManagerListener)
}
private fun refreshModel() = withState { state ->
if (state.identityServer().isNullOrBlank()) return@withState
setState {
copy(
emailList = Loading(),
phoneNumbersList = Loading()
)
}
/* TODO
session.refreshThirdPartyIdentifiers(object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
_errorLiveEvent.postValue(LiveEvent(failure))
setState {
copy(
emailList = Fail(failure),
phoneNumbersList = Fail(failure)
)
}
}
override fun onSuccess(data: Unit) {
setState {
copy(termsNotSigned = false)
}
retrieveBinding()
}
})
*/
}
private fun retrieveBinding() {
/* TODO
val linkedMailsInfo = session.myUser.getlinkedEmails()
val knownEmails = linkedMailsInfo.map { it.address }
// Note: it will be a list of "email"
val knownEmailMedium = linkedMailsInfo.map { it.medium }
val linkedMsisdnsInfo = session.myUser.getlinkedPhoneNumbers()
val knownMsisdns = linkedMsisdnsInfo.map { it.address }
// Note: it will be a list of "msisdn"
val knownMsisdnMedium = linkedMsisdnsInfo.map { it.medium }
setState {
copy(
emailList = Success(knownEmails.map { PidInfo(it, Loading()) }),
phoneNumbersList = Success(knownMsisdns.map { PidInfo(it, Loading()) })
)
}
identityService.lookup(knownEmails + knownMsisdns,
knownEmailMedium + knownMsisdnMedium,
object : MatrixCallback<List<FoundThreePid>> {
override fun onSuccess(data: List<FoundThreePid>) {
setState {
copy(
emailList = Success(toPidInfoList(knownEmails, data.take(knownEmails.size))),
phoneNumbersList = Success(toPidInfoList(knownMsisdns, data.takeLast(knownMsisdns.size)))
)
}
}
override fun onUnexpectedError(e: Exception) {
if (e is TermsNotSignedException) {
setState {
// TODO Use ViewEvent
copy(termsNotSigned = true)
}
}
onError(e)
}
override fun onNetworkError(e: Exception) {
onError(e)
}
override fun onMatrixError(e: MatrixError) {
onError(Throwable(e.message))
}
private fun onError(e: Throwable) {
_errorLiveEvent.postValue(LiveEvent(e))
setState {
copy(
emailList = Success(knownEmails.map { PidInfo(it, Fail(e)) }),
phoneNumbersList = Success(knownMsisdns.map { PidInfo(it, Fail(e)) })
)
}
}
})
*/
}
private fun toPidInfoList(addressList: List<String>, matrixIds: List<String>): List<PidInfo> {
return addressList.map {
val hasMatrixId = matrixIds[addressList.indexOf(it)].isNotBlank()
PidInfo(
value = it,
isShared = Success(PidInfo.SharedState.SHARED.takeIf { hasMatrixId } ?: PidInfo.SharedState.NOT_SHARED)
)
}
}
private fun submitMsisdnToken(action: DiscoverySettingsAction.SubmitMsisdnToken) = withState { state ->
val pid = state.phoneNumbersList()?.find { it.value == action.msisdn }?._3pid ?: return@withState
identityService.submitValidationToken(pid,
action.code,
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(ThreePid.Msisdn(action.msisdn), action.bind))
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
changeMsisdnState(action.msisdn, Fail(failure))
}
}
)
}
private fun finalizeBind3pid(action: DiscoverySettingsAction.FinalizeBind3pid) = withState { state ->
val _3pid = when (action.threePid) {
is ThreePid.Email -> {
changeMailState(action.threePid.email, Loading())
state.emailList()?.find { it.value == action.threePid.email }?._3pid ?: return@withState
}
is ThreePid.Msisdn -> {
changeMsisdnState(action.threePid.msisdn, Loading())
state.phoneNumbersList()?.find { it.value == action.threePid.msisdn }?._3pid ?: return@withState
}
}
identityService.finalizeBindSessionFor3PID(_3pid, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
val sharedState = Success(if (action.bind) PidInfo.SharedState.SHARED else PidInfo.SharedState.NOT_SHARED)
when (action.threePid) {
is ThreePid.Email -> changeMailState(action.threePid.email, sharedState, null)
is ThreePid.Msisdn -> changeMsisdnState(action.threePid.msisdn, sharedState, null)
}
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
// Restore previous state after an error
val sharedState = Success(if (action.bind) PidInfo.SharedState.NOT_VERIFIED_FOR_BIND else PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND)
when (action.threePid) {
is ThreePid.Email -> changeMailState(action.threePid.email, sharedState)
is ThreePid.Msisdn -> changeMsisdnState(action.threePid.msisdn, sharedState)
}
}
})
}
private fun refreshPendingEmailBindings() = withState { state ->
state.emailList()?.forEach { info ->
when (info.isShared()) {
PidInfo.SharedState.NOT_VERIFIED_FOR_BIND -> finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(ThreePid.Email(info.value), true))
PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND -> finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(ThreePid.Email(info.value), false))
else -> Unit
}
}
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import im.vector.riotx.core.utils.LiveEvent
import javax.inject.Inject
// TODO Rework this
class DiscoverySharedViewModel @Inject constructor() : ViewModel() {
var navigateEvent = MutableLiveData<LiveEvent<Pair<String, String>>>()
companion object {
const val NEW_IDENTITY_SERVER_SET_REQUEST = "NEW_IDENTITY_SERVER_SET_REQUEST"
}
fun requestChangeToIdentityServer(server: String) {
navigateEvent.postValue(LiveEvent(NEW_IDENTITY_SERVER_SET_REQUEST to server))
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery
import android.view.View
import android.widget.Button
import androidx.annotation.StringRes
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.resources.ColorProvider
@EpoxyModelClass(layout = R.layout.item_settings_button)
abstract class SettingsButtonItem : EpoxyModelWithHolder<SettingsButtonItem.Holder>() {
@EpoxyAttribute
lateinit var colorProvider: ColorProvider
@EpoxyAttribute
var buttonTitle: String? = null
@EpoxyAttribute
@StringRes
var buttonTitleId: Int? = null
@EpoxyAttribute
var buttonStyle: SettingsTextButtonItem.ButtonStyle = SettingsTextButtonItem.ButtonStyle.POSITIVE
@EpoxyAttribute
var buttonClickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
if (buttonTitleId != null) {
holder.button.setText(buttonTitleId!!)
} else {
holder.button.setTextOrHide(buttonTitle)
}
when (buttonStyle) {
SettingsTextButtonItem.ButtonStyle.POSITIVE -> {
holder.button.setTextColor(colorProvider.getColorFromAttribute(R.attr.colorAccent))
}
SettingsTextButtonItem.ButtonStyle.DESTRUCTIVE -> {
holder.button.setTextColor(colorProvider.getColor(R.color.vector_error_color))
}
}
holder.button.setOnClickListener(buttonClickListener)
}
class Holder : VectorEpoxyHolder() {
val button by bind<Button>(R.id.settings_item_button)
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_settings_radio_single_line)
abstract class SettingsImageItem : EpoxyModelWithHolder<SettingsImageItem.Holder>() {
@EpoxyAttribute
var title: String? = null
@EpoxyAttribute
@StringRes
var titleResId: Int? = null
@EpoxyAttribute
@DrawableRes
var endIconResourceId: Int = -1
@EpoxyAttribute
var itemClickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
if (titleResId != null) {
holder.textView.setText(titleResId!!)
} else {
holder.textView.setTextOrHide(title)
}
if (endIconResourceId != -1) {
holder.accessoryImage.setImageResource(endIconResourceId)
holder.accessoryImage.isVisible = true
} else {
holder.accessoryImage.isVisible = false
}
holder.view.setOnClickListener(itemClickListener)
}
class Holder : VectorEpoxyHolder() {
val textView by bind<TextView>(R.id.settings_item_text)
val accessoryImage by bind<ImageView>(R.id.settings_item_image)
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery
import android.view.View
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_settings_helper_info)
abstract class SettingsInfoItem : EpoxyModelWithHolder<SettingsInfoItem.Holder>() {
@EpoxyAttribute
var helperText: String? = null
@EpoxyAttribute
@StringRes
var helperTextResId: Int? = null
@EpoxyAttribute
var itemClickListener: View.OnClickListener? = null
@EpoxyAttribute
@DrawableRes
var compoundDrawable: Int = R.drawable.vector_warning_red
@EpoxyAttribute
var showCompoundDrawable: Boolean = false
override fun bind(holder: Holder) {
super.bind(holder)
if (helperTextResId != null) {
holder.text.setText(helperTextResId!!)
} else {
holder.text.setTextOrHide(helperText)
}
holder.view.setOnClickListener(itemClickListener)
if (showCompoundDrawable) {
holder.text.setCompoundDrawablesWithIntrinsicBounds(compoundDrawable, 0, 0, 0)
} else {
holder.text.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
}
}
class Holder : VectorEpoxyHolder() {
val text by bind<TextView>(R.id.settings_helper_text)
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery
import android.view.View
import android.widget.Switch
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_settings_simple_item)
abstract class SettingsItem : EpoxyModelWithHolder<SettingsItem.Holder>() {
@EpoxyAttribute
var title: String? = null
@EpoxyAttribute
@StringRes
var titleResId: Int? = null
@EpoxyAttribute
@StringRes
var descriptionResId: Int? = null
@EpoxyAttribute
var description: CharSequence? = null
@EpoxyAttribute
var itemClickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
if (titleResId != null) {
holder.titleText.setText(titleResId!!)
} else {
holder.titleText.setTextOrHide(title)
}
if (descriptionResId != null) {
holder.descriptionText.setText(descriptionResId!!)
} else {
holder.descriptionText.setTextOrHide(description)
}
//If there is only a description, use primary color
// holder.descriptionText.setTextColor(
// if (holder.titleText.text.isNullOrBlank()) {
// ThemeUtils.getColor(holder.main.context, android.R.attr.textColorPrimary)
// } else {
// ThemeUtils.getColor(holder.main.context, android.R.attr.textColorSecondary)
// }
// )
holder.switchButton.isVisible = false
holder.view.setOnClickListener(itemClickListener)
}
class Holder : VectorEpoxyHolder() {
val titleText by bind<TextView>(R.id.settings_item_title)
val descriptionText by bind<TextView>(R.id.settings_item_description)
val switchButton by bind<Switch>(R.id.settings_item_switch)
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery
import android.view.inputmethod.EditorInfo
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import com.google.android.material.textfield.TextInputLayout
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_settings_edit_text)
abstract class SettingsItemText : EpoxyModelWithHolder<SettingsItemText.Holder>() {
@EpoxyAttribute var descriptionText: String? = null
@EpoxyAttribute var errorText: String? = null
@EpoxyAttribute
var interactionListener: Listener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.textView.setTextOrHide(descriptionText)
holder.validateButton.setOnClickListener {
val code = holder.editText.text.toString()
holder.editText.text.clear()
interactionListener?.onValidate(code)
}
if (errorText.isNullOrBlank()) {
holder.textInputLayout.error = null
} else {
holder.textInputLayout.error = errorText
}
holder.editText.setOnEditorActionListener { tv, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
val code = tv.text.toString()
interactionListener?.onValidate(code)
holder.editText.text.clear()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
}
class Holder : VectorEpoxyHolder() {
val textView by bind<TextView>(R.id.settings_item_description)
val editText by bind<EditText>(R.id.settings_item_edittext)
val textInputLayout by bind<TextInputLayout>(R.id.settings_item_enter_til)
val validateButton by bind<Button>(R.id.settings_item_enter_button)
}
interface Listener {
fun onValidate(code: String)
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_loading)
abstract class SettingsLoadingItem : EpoxyModelWithHolder<SettingsLoadingItem.Holder>() {
@EpoxyAttribute var loadingText: String? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.textView.setTextOrHide(loadingText)
holder.progressBar.isVisible = true
holder.progressBar.animate()
}
class Holder : VectorEpoxyHolder() {
val textView by bind<TextView>(R.id.loadingText)
val progressBar by bind<ProgressBar>(R.id.loadingProgress)
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery
import android.widget.TextView
import androidx.annotation.StringRes
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_settings_section_title)
abstract class SettingsSectionTitle : EpoxyModelWithHolder<SettingsSectionTitle.Holder>() {
@EpoxyAttribute
var title: String? = null
@EpoxyAttribute
@StringRes
var titleResId: Int? = null
override fun bind(holder: Holder) {
super.bind(holder)
if (titleResId != null) {
holder.textView.setText(titleResId!!)
} else {
holder.textView.setTextOrHide(title)
}
}
class Holder : VectorEpoxyHolder() {
val textView by bind<TextView>(R.id.settings_section_title_text)
}
}

View File

@ -0,0 +1,173 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery
import android.view.View
import android.widget.Button
import android.widget.CompoundButton
import android.widget.ProgressBar
import android.widget.Switch
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.themes.ThemeUtils
@EpoxyModelClass(layout = R.layout.item_settings_button_single_line)
abstract class SettingsTextButtonItem : EpoxyModelWithHolder<SettingsTextButtonItem.Holder>() {
enum class ButtonStyle {
POSITIVE,
DESTRUCTIVE
}
enum class ButtonType {
NORMAL,
SWITCH
}
@EpoxyAttribute
lateinit var colorProvider: ColorProvider
@EpoxyAttribute
lateinit var stringProvider: StringProvider
@EpoxyAttribute
var title: String? = null
@EpoxyAttribute
@StringRes
var titleResId: Int? = null
@EpoxyAttribute
var buttonTitle: String? = null
@EpoxyAttribute
@StringRes
var buttonTitleId: Int? = null
@EpoxyAttribute
var buttonStyle: ButtonStyle = ButtonStyle.POSITIVE
@EpoxyAttribute
var buttonType: ButtonType = ButtonType.NORMAL
@EpoxyAttribute
var buttonIndeterminate: Boolean = false
@EpoxyAttribute
var checked: Boolean? = null
@EpoxyAttribute
var buttonClickListener: View.OnClickListener? = null
@EpoxyAttribute
var switchChangeListener: CompoundButton.OnCheckedChangeListener? = null
@EpoxyAttribute
var infoMessage: String? = null
@EpoxyAttribute
@StringRes
var infoMessageId: Int? = null
@EpoxyAttribute
@ColorRes
var infoMessageTintColorId: Int = R.color.vector_error_color
override fun bind(holder: Holder) {
super.bind(holder)
if (titleResId != null) {
holder.textView.setText(titleResId!!)
} else {
holder.textView.setTextOrHide(title, hideWhenBlank = false)
}
if (buttonTitleId != null) {
holder.button.setText(buttonTitleId!!)
} else {
holder.button.setTextOrHide(buttonTitle)
}
if (buttonIndeterminate) {
holder.spinner.isVisible = true
holder.button.isInvisible = true
holder.switchButton.isInvisible = true
holder.switchButton.setOnCheckedChangeListener(null)
holder.button.setOnClickListener(null)
} else {
holder.spinner.isVisible = false
when (buttonType) {
ButtonType.NORMAL -> {
holder.button.isVisible = true
holder.switchButton.isVisible = false
when (buttonStyle) {
ButtonStyle.POSITIVE -> {
holder.button.setTextColor(colorProvider.getColorFromAttribute(R.attr.colorAccent))
}
ButtonStyle.DESTRUCTIVE -> {
holder.button.setTextColor(colorProvider.getColor(R.color.vector_error_color))
}
}
holder.button.setOnClickListener(buttonClickListener)
}
ButtonType.SWITCH -> {
holder.button.isVisible = false
holder.switchButton.isVisible = true
//set to null before changing the state
holder.switchButton.setOnCheckedChangeListener(null)
checked?.let { holder.switchButton.isChecked = it }
holder.switchButton.setOnCheckedChangeListener(switchChangeListener)
}
}
}
val errorMessage = infoMessageId?.let { stringProvider.getString(it) } ?: infoMessage
if (errorMessage != null) {
holder.errorTextView.isVisible = true
holder.errorTextView.setTextOrHide(errorMessage)
val errorColor = colorProvider.getColor(infoMessageTintColorId)
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_notification_privacy_warning)?.apply {
ThemeUtils.tintDrawableWithColor(this, errorColor)
holder.textView.setCompoundDrawablesWithIntrinsicBounds(this, null, null, null)
}
holder.errorTextView.setTextColor(errorColor)
} else {
holder.errorTextView.isVisible = false
holder.errorTextView.text = null
holder.textView.setCompoundDrawables(null, null, null, null)
}
}
class Holder : VectorEpoxyHolder() {
val textView by bind<TextView>(R.id.settings_item_text)
val button by bind<Button>(R.id.settings_item_button)
val switchButton by bind<Switch>(R.id.settings_item_switch)
val spinner by bind<ProgressBar>(R.id.settings_item_button_spinner)
val errorTextView by bind<TextView>(R.id.settings_item_error_message)
}
}

View File

@ -0,0 +1,170 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery.change
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ProgressBar
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import butterknife.BindView
import butterknife.OnTextChanged
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.textfield.TextInputLayout
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.discovery.DiscoverySharedViewModel
import javax.inject.Inject
class SetIdentityServerFragment @Inject constructor(
val viewModelFactory: SetIdentityServerViewModel.Factory
) : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_set_identity_server
override fun getMenuRes() = R.menu.menu_phone_number_addition
@BindView(R.id.discovery_identity_server_enter_til)
lateinit var mKeyInputLayout: TextInputLayout
@BindView(R.id.discovery_identity_server_enter_edittext)
lateinit var mKeyTextEdit: EditText
@BindView(R.id.discovery_identity_server_loading)
lateinit var mProgressBar: ProgressBar
private val viewModel by fragmentViewModel(SetIdentityServerViewModel::class)
lateinit var sharedViewModel: DiscoverySharedViewModel
override fun invalidate() = withState(viewModel) { state ->
if (state.isVerifyingServer) {
mKeyTextEdit.isEnabled = false
mProgressBar.isVisible = true
} else {
mKeyTextEdit.isEnabled = true
mProgressBar.isVisible = false
}
val newText = state.newIdentityServer ?: ""
if (!newText.equals(mKeyTextEdit.text.toString())) {
mKeyTextEdit.setText(newText)
}
mKeyInputLayout.error = state.errorMessageId?.let { getString(it) }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
// TODO Create another menu
R.id.action_add_phone_number -> {
withState(viewModel) { state ->
if (!state.isVerifyingServer) {
viewModel.handle(SetIdentityServerAction.DoChangeServerName)
}
}
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel = activityViewModelProvider.get(DiscoverySharedViewModel::class.java)
mKeyTextEdit.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
withState(viewModel) { state ->
if (!state.isVerifyingServer) {
viewModel.handle(SetIdentityServerAction.DoChangeServerName)
}
}
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
viewModel.observeViewEvents {
when (it) {
is SetIdentityServerViewEvents.NoTerms -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_discovery_no_terms_title)
.setMessage(R.string.settings_discovery_no_terms)
.setPositiveButton(R.string._continue) { _, _ ->
processIdentityServerChange()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
is SetIdentityServerViewEvents.TermsAccepted -> {
processIdentityServerChange()
}
is SetIdentityServerViewEvents.ShowTerms -> {
/* TODO
ReviewTermsActivity.intent(requireContext(),
TermsManager.ServiceType.IdentityService,
SetIdentityServerViewModel.sanitatizeBaseURL(event.newIdentityServer),
null).also {
startActivityForResult(it, TERMS_REQUEST_CODE)
}
*/
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
/* TODO
if (requestCode == TERMS_REQUEST_CODE) {
if (Activity.RESULT_OK == resultCode) {
processIdentityServerChange()
} else {
//add some error?
}
}
*/
super.onActivityResult(requestCode, resultCode, data)
}
private fun processIdentityServerChange() {
withState(viewModel) { state ->
if (state.newIdentityServer != null) {
sharedViewModel.requestChangeToIdentityServer(state.newIdentityServer)
parentFragmentManager.popBackStack()
}
}
}
@OnTextChanged(R.id.discovery_identity_server_enter_edittext)
fun onTextEditChange(s: Editable?) {
s?.toString()?.let { viewModel.handle(SetIdentityServerAction.UpdateServerName(it)) }
}
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.identity_server)
}
}

View File

@ -0,0 +1,171 @@
/*
* Copyright (c) 2020 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.riotx.features.discovery.change
import androidx.annotation.StringRes
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.R
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.core.resources.StringProvider
data class SetIdentityServerState(
val existingIdentityServer: String? = null,
val newIdentityServer: String? = null,
@StringRes val errorMessageId: Int? = null,
val isVerifyingServer: Boolean = false
) : MvRxState
sealed class SetIdentityServerAction : VectorViewModelAction {
data class UpdateServerName(val url: String) : SetIdentityServerAction()
object DoChangeServerName : SetIdentityServerAction()
}
sealed class SetIdentityServerViewEvents : VectorViewEvents {
data class ShowTerms(val newIdentityServer: String) : SetIdentityServerViewEvents()
object NoTerms : SetIdentityServerViewEvents()
object TermsAccepted : SetIdentityServerViewEvents()
}
class SetIdentityServerViewModel @AssistedInject constructor(
@Assisted initialState: SetIdentityServerState,
private val mxSession: Session,
stringProvider: StringProvider)
: VectorViewModel<SetIdentityServerState, SetIdentityServerAction, SetIdentityServerViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: SetIdentityServerState): SetIdentityServerViewModel
}
companion object : MvRxViewModelFactory<SetIdentityServerViewModel, SetIdentityServerState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: SetIdentityServerState): SetIdentityServerViewModel? {
val fragment: SetIdentityServerFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewModelFactory.create(state)
}
fun sanitatizeBaseURL(baseUrl: String): String {
var baseUrl1 = baseUrl
if (!baseUrl1.startsWith("http://") && !baseUrl1.startsWith("https://")) {
baseUrl1 = "https://$baseUrl1"
}
return baseUrl1
}
}
val userLanguage = stringProvider.getString(R.string.resources_language)
override fun handle(action: SetIdentityServerAction) {
when (action) {
is SetIdentityServerAction.UpdateServerName -> updateServerName(action)
SetIdentityServerAction.DoChangeServerName -> doChangeServerName()
}.exhaustive
}
private fun updateServerName(action: SetIdentityServerAction.UpdateServerName) {
setState {
copy(
newIdentityServer = action.url,
errorMessageId = null
)
}
}
private fun doChangeServerName() = withState {
var baseUrl: String? = it.newIdentityServer
if (baseUrl.isNullOrBlank()) {
setState {
copy(errorMessageId = R.string.settings_discovery_please_enter_server)
}
return@withState
}
// TODO baseUrl = sanitatizeBaseURL(baseUrl)
setState {
copy(isVerifyingServer = true)
}
/* TODO
mxSession.termsManager.get(TermsManager.ServiceType.IdentityService,
baseUrl,
object : ApiCallback<GetTermsResponse> {
override fun onSuccess(info: GetTermsResponse) {
//has all been accepted?
setState {
copy(isVerifyingServer = false)
}
val resp = info.serverResponse
val tos = resp.getLocalizedTerms(userLanguage)
if (tos.isEmpty()) {
//prompt do not define policy
navigateEvent.value = LiveEvent(NavigateEvent.NoTerms)
} else {
val shouldPrompt = tos.any { !info.alreadyAcceptedTermUrls.contains(it.localizedUrl) }
if (shouldPrompt) {
navigateEvent.value = LiveEvent(NavigateEvent.ShowTerms(baseUrl))
} else {
navigateEvent.value = LiveEvent(NavigateEvent.TermsAccepted)
}
}
}
override fun onUnexpectedError(e: Exception) {
if (e is HttpException && e.httpError.httpCode == 404) {
setState {
copy(isVerifyingServer = false)
}
navigateEvent.value = LiveEvent(NavigateEvent.NoTerms)
} else {
setState {
copy(
isVerifyingServer = false,
errorMessageId = R.string.settings_discovery_bad_identity_server
)
}
}
}
override fun onNetworkError(e: Exception) {
setState {
copy(
isVerifyingServer = false,
errorMessageId = R.string.settings_discovery_bad_identity_server
)
}
}
override fun onMatrixError(e: MatrixError) {
setState {
copy(
isVerifyingServer = false,
errorMessageId = R.string.settings_discovery_bad_identity_server
)
}
}
})
*/
}
}

View File

@ -165,8 +165,9 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
.summary = session.sessionParams.homeServerConnectionConfig.homeServerUri.toString()
// identity server
// TODO Handle refresh of the value
findPreference<VectorPreference>(VectorPreferences.SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY)!!
.summary = session.sessionParams.homeServerConnectionConfig.identityServerUri.toString()
.summary = session.identityService().getCurrentIdentityServer() ?: getString(R.string.identity_server_not_defined)
refreshEmailsList()
refreshPhoneNumbersList()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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="match_parent"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/discovery_identity_server_enter_til"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:errorEnabled="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/discovery_identity_server_enter_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/settings_discovery_enter_identity_server"
android:imeOptions="actionDone"
android:inputType="textUri"
android:maxLines="3"
android:textColor="?android:textColorPrimary"
tools:text="vector.im" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/discovery_identity_server_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/discovery_identity_server_enter_til"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/settings_item_button"
style="@style/VectorButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:text="@string/action_change" />
</FrameLayout>

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorBackgroundFloating"
android:foreground="?attr/selectableItemBackground"
android:orientation="vertical"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/settings_item_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:orientation="vertical"
android:textColor="?android:textColorPrimary"
android:textSize="15sp"
tools:drawableLeft="@drawable/ic_notification_privacy_warning"
tools:drawableStart="@drawable/ic_notification_privacy_warning"
tools:drawableTint="@color/vector_error_color"
tools:text="foo@bar.test" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="40dp"
android:minWidth="70dp">
<Button
android:id="@+id/settings_item_button"
style="@style/VectorButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
tools:text="@string/share"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/settings_item_button_spinner"
style="?android:attr/progressBarStyle"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:visibility="invisible"
tools:visibility="invisible" />
<Switch
android:id="@+id/settings_item_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
</LinearLayout>
<TextView
android:id="@+id/settings_item_error_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:textSize="12sp"
android:visibility="gone"
tools:drawableStart="@drawable/ic_notification_privacy_warning"
tools:text="Error Message"
tools:textColor="@color/vector_info_color"
tools:visibility="visible">
</TextView>
</LinearLayout>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="?attr/colorBackgroundFloating"
android:orientation="vertical"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/settings_item_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:textColor="?android:textColorSecondary"
android:textSize="15sp"
tools:text="@string/settings_text_message_sent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/settings_item_enter_til"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/settings_item_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone|flagNoPersonalizedLearning"
android:inputType="numberDecimal"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
tools:text="1234" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<Button
android:id="@+id/settings_item_enter_button"
style="@style/VectorButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/_continue" />
</LinearLayout>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/settings_helper_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:drawablePadding="8dp"
android:orientation="vertical"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:textColor="?android:textColorSecondary"
android:textSize="14sp"
tools:drawableStart="@drawable/vector_warning_red"
tools:text="If you dont want this, opt out below. You can also manage any of these preferences in Settings." />

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorBackgroundFloating"
android:orientation="horizontal"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">
<FrameLayout
android:layout_width="40dp"
android:layout_height="40dp">
<ImageView
android:id="@+id/settings_item_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:src="@drawable/unit_test" />
</FrameLayout>
<TextView
android:id="@+id/settings_item_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_weight="1"
android:orientation="vertical"
android:textColor="?android:textColorPrimary"
android:textSize="15sp"
tools:text="An option" />
</LinearLayout>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/settings_section_title_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="4dp"
android:orientation="vertical"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:textColor="?android:textColorPrimary"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Title" />

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorBackgroundFloating"
android:orientation="horizontal"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/settings_item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:orientation="vertical"
android:textColor="?android:textColorPrimary"
android:textSize="15sp"
android:textStyle="bold"
tools:text="Title" />
<TextView
android:id="@+id/settings_item_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:orientation="vertical"
android:textColor="?android:textColorSecondary"
android:textSize="15sp"
tools:text="Description / Value" />
</LinearLayout>
<Switch
android:id="@+id/settings_item_switch"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center" />
</LinearLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_add_phone_number"
android:icon="@drawable/ic_material_done_white"
android:title="@string/settings_add_phone_number"
app:showAsAction="always" />
</menu>

View File

@ -5,6 +5,7 @@
<color name="vector_success_color">#70BF56</color>
<color name="vector_warning_color">#ff4b55</color>
<color name="vector_error_color">#ff4b55</color>
<color name="vector_info_color">#2f9edb</color>
<!-- main app colors -->
<color name="vector_fuchsia_color">#ff4b55</color>

View File

@ -43,6 +43,13 @@
android:title="@string/settings_add_phone_number"
app:iconTint="?attr/vctr_settings_icon_tint_color" />
<im.vector.riotx.core.preference.VectorPreference
android:order="1000"
android:persistent="false"
android:summary="@string/settings_discovery_manage"
android:title="@string/settings_discovery_category"
app:fragment="im.vector.riotx.features.discovery.DiscoverySettingsFragment" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceCategory
@ -76,7 +83,7 @@
<im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY"
android:title="@string/settings_identity_server"
app:isPreferenceVisible="@bool/false_not_implemented"
app:fragment="im.vector.riotx.features.discovery.DiscoverySettingsFragment"
tools:summary="https://identity.server.url" />
<im.vector.riotx.core.preference.VectorPreference