From e5da026f1f20aed42cd291ee08a82668f514e7a3 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 10 Feb 2021 10:05:41 +0100 Subject: [PATCH 01/62] Dev tools initial commit --- CHANGES.md | 2 +- .../room/model/RoomThirdPartyInviteContent.kt | 8 +- .../room/send/queue/EventSenderProcessor.kt | 1 + .../room/state/StateEventDataSource.kt | 6 +- vector/src/main/AndroidManifest.xml | 1 + .../im/vector/app/core/di/FragmentModule.kt | 24 ++ .../im/vector/app/core/di/ScreenComponent.kt | 2 + .../im/vector/app/core/ui/list/GenericItem.kt | 2 +- .../features/devtools/DevToolsViewEvents.kt | 27 ++ .../features/devtools/RoomDevToolAction.kt | 34 ++ .../features/devtools/RoomDevToolActivity.kt | 247 +++++++++++++++ .../devtools/RoomDevToolEditFragment.kt | 72 +++++ .../features/devtools/RoomDevToolFragment.kt | 72 +++++ .../devtools/RoomDevToolRootController.kt | 60 ++++ .../devtools/RoomDevToolSendFormController.kt | 76 +++++ .../devtools/RoomDevToolSendFormFragment.kt | 62 ++++ .../RoomDevToolStateEventListFragment.kt | 62 ++++ .../features/devtools/RoomDevToolViewModel.kt | 293 ++++++++++++++++++ .../features/devtools/RoomDevToolViewState.kt | 52 ++++ .../devtools/RoomStateListController.kt | 113 +++++++ .../form/FormMultiLineEditTextItem.kt | 101 ++++++ .../home/room/detail/RoomDetailFragment.kt | 5 + .../home/room/detail/RoomDetailViewModel.kt | 1 + .../members/RoomMemberListController.kt | 3 +- .../res/layout/fragment_devtools_editor.xml | 16 + .../layout/item_form_multiline_text_input.xml | 45 +++ .../src/main/res/layout/item_generic_list.xml | 1 + vector/src/main/res/menu/menu_devtools.xml | 22 ++ vector/src/main/res/menu/menu_timeline.xml | 8 + vector/src/main/res/values/strings.xml | 1 + 30 files changed, 1411 insertions(+), 8 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/devtools/DevToolsViewEvents.kt create mode 100644 vector/src/main/java/im/vector/app/features/devtools/RoomDevToolAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/devtools/RoomDevToolEditFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/devtools/RoomDevToolFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/devtools/RoomDevToolRootController.kt create mode 100644 vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt create mode 100644 vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/devtools/RoomDevToolStateEventListFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewState.kt create mode 100644 vector/src/main/java/im/vector/app/features/devtools/RoomStateListController.kt create mode 100644 vector/src/main/java/im/vector/app/features/form/FormMultiLineEditTextItem.kt create mode 100644 vector/src/main/res/layout/fragment_devtools_editor.xml create mode 100644 vector/src/main/res/layout/item_form_multiline_text_input.xml create mode 100644 vector/src/main/res/menu/menu_devtools.xml diff --git a/CHANGES.md b/CHANGES.md index df4728c634..c1d7998e78 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,7 +23,7 @@ Test: - Other changes: - - + - New Dev Tools panel for developers Changes in Element 1.0.17 (2020-02-09) =================================================== diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt index 776acbd8ea..56503e3e35 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt @@ -30,24 +30,24 @@ data class RoomThirdPartyInviteContent( * This should not contain the user's third party ID, as otherwise when the invite * is accepted it would leak the association between the matrix ID and the third party ID. */ - @Json(name = "display_name") val displayName: String, + @Json(name = "display_name") val displayName: String?, /** * Required. A URL which can be fetched, with querystring public_key=public_key, to validate * whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'. */ - @Json(name = "key_validity_url") val keyValidityUrl: String, + @Json(name = "key_validity_url") val keyValidityUrl: String?, /** * Required. A base64-encoded ed25519 key with which token must be signed (though a signature from any entry in * public_keys is also sufficient). This exists for backwards compatibility. */ - @Json(name = "public_key") val publicKey: String, + @Json(name = "public_key") val publicKey: String?, /** * Keys with which the token may be signed. */ - @Json(name = "public_keys") val publicKeys: List = emptyList() + @Json(name = "public_keys") val publicKeys: List? = emptyList() ) @JsonClass(generateAdapter = true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt index 5014d94558..62338a1d07 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt @@ -196,6 +196,7 @@ internal class EventSenderProcessor @Inject constructor( else -> { Timber.v("## SendThread retryLoop Un-Retryable error, try next task") // this task is in error, check next one? + task.onTaskFailed() break@retryLoop } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt index d0f6f8050e..a25a362bfa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt @@ -80,7 +80,11 @@ internal class StateEventDataSource @Inject constructor(@SessionDatabase private ): RealmQuery { return realm.where() .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) - .`in`(CurrentStateEventEntityFields.TYPE, eventTypes.toTypedArray()) + .apply { + if (eventTypes.isNotEmpty()) { + `in`(CurrentStateEventEntityFields.TYPE, eventTypes.toTypedArray()) + } + } .process(CurrentStateEventEntityFields.STATE_KEY, stateKey) } } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index bfaea39cc6..6f2ff4e8ca 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -262,6 +262,7 @@ + () { } @EpoxyAttribute - var title: String? = null + var title: CharSequence? = null @EpoxyAttribute var description: CharSequence? = null diff --git a/vector/src/main/java/im/vector/app/features/devtools/DevToolsViewEvents.kt b/vector/src/main/java/im/vector/app/features/devtools/DevToolsViewEvents.kt new file mode 100644 index 0000000000..615144aaf6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/devtools/DevToolsViewEvents.kt @@ -0,0 +1,27 @@ +/* + * 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.devtools + +import im.vector.app.core.platform.VectorViewEvents + +sealed class DevToolsViewEvents : VectorViewEvents { + object Dismiss : DevToolsViewEvents() + + // object ShowStateList : DevToolsViewEvents() + data class showAlertMessage(val message: String) : DevToolsViewEvents() + data class showSnackMessage(val message: String) : DevToolsViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolAction.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolAction.kt new file mode 100644 index 0000000000..c6246bbe08 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolAction.kt @@ -0,0 +1,34 @@ +/* + * 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.devtools + +import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.events.model.Event + +sealed class RoomDevToolAction : VectorViewModelAction { + object ExploreRoomState : RoomDevToolAction() + object OnBackPressed : RoomDevToolAction() + object MenuEdit : RoomDevToolAction() + object MenuItemSend : RoomDevToolAction() + data class ShowStateEvent(val event: Event) : RoomDevToolAction() + data class ShowStateEventType(val stateEventType: String) : RoomDevToolAction() + data class UpdateContentText(val contentJson: String) : RoomDevToolAction() + data class SendCustomEvent(val isStateEvent: Boolean) : RoomDevToolAction() + data class CustomEventTypeChange(val type: String) : RoomDevToolAction() + data class CustomEventContentChange(val content: String) : RoomDevToolAction() + data class CustomEventStateKeyChange(val stateKey: String) : RoomDevToolAction() +} diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt new file mode 100644 index 0000000000..fe6d684474 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt @@ -0,0 +1,247 @@ +/* + * 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.devtools + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.app.AlertDialog +import androidx.core.view.forEach +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.viewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.replaceFragment +import im.vector.app.core.extensions.toMvRxBundle +import im.vector.app.core.platform.SimpleFragmentActivity +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.utils.createJSonViewerStyleProvider +import kotlinx.parcelize.Parcelize +import org.billcarsonfr.jsonviewer.JSonViewerFragment +import javax.inject.Inject + +class RoomDevToolActivity : SimpleFragmentActivity(), RoomDevToolViewModel.Factory, + FragmentManager.OnBackStackChangedListener { + + @Inject lateinit var viewModelFactory: RoomDevToolViewModel.Factory + @Inject lateinit var colorProvider: ColorProvider + + // private lateinit var viewModel: RoomDevToolViewModel + private val viewModel: RoomDevToolViewModel by viewModel() + + override fun getTitleRes() = R.string.dev_tools_menu_name + + override fun getMenuRes() = R.menu.menu_devtools + + var currentDisplayMode: RoomDevToolViewState.Mode? = null + + @Parcelize + data class Args( + val roomId: String + ) : Parcelable + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + override fun create(initialState: RoomDevToolViewState): RoomDevToolViewModel { + return viewModelFactory.create(initialState) + } + + override fun initUiAndData() { + super.initUiAndData() + viewModel.subscribe(this) { + renderState(it) + } + + viewModel.observeViewEvents { + when (it) { + DevToolsViewEvents.Dismiss -> finish() + is DevToolsViewEvents.showAlertMessage -> { + AlertDialog.Builder(this) + .setMessage(it.message) + .setPositiveButton(R.string.ok, null) + .show() + } + is DevToolsViewEvents.showSnackMessage -> showSnackbar(it.message) + } + } + supportFragmentManager.addOnBackStackChangedListener(this) + } + + private fun renderState(it: RoomDevToolViewState) { + if (it.displayMode != currentDisplayMode) { + when (it.displayMode) { + RoomDevToolViewState.Mode.Root -> { + val classJava = RoomDevToolFragment::class.java + val tag = classJava.name + if (supportFragmentManager.findFragmentByTag(tag) == null) { + replaceFragment(R.id.container, RoomDevToolFragment::class.java) + } else { + supportFragmentManager.popBackStack() + } + } + RoomDevToolViewState.Mode.StateEventDetail -> { + val frag = JSonViewerFragment.newInstance(it.selectedEventJson ?: "", -1, true, createJSonViewerStyleProvider(colorProvider)) + navigateTo(frag) + } + RoomDevToolViewState.Mode.StateEventList, + RoomDevToolViewState.Mode.StateEventListByType -> { + val frag = createFragment(RoomDevToolStateEventListFragment::class.java, Bundle().toMvRxBundle()) + navigateTo(frag) + } + RoomDevToolViewState.Mode.EditEventContent -> { + val frag = createFragment(RoomDevToolEditFragment::class.java, Bundle().toMvRxBundle()) + navigateTo(frag) + } + is RoomDevToolViewState.Mode.SendEventForm -> { + val frag = createFragment(RoomDevToolSendFormFragment::class.java, Bundle().toMvRxBundle()) + navigateTo(frag) + } + } + currentDisplayMode = it.displayMode + invalidateOptionsMenu() + } + + when (it.modalLoading) { + is Loading -> showWaitingView() + is Success -> hideWaitingView() + is Fail -> { + hideWaitingView() + } + Uninitialized -> { + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressed() + return true + } + if (item.itemId == R.id.menuItemEdit) { + viewModel.handle(RoomDevToolAction.MenuEdit) + return true + } + if (item.itemId == R.id.menuItemSend) { + viewModel.handle(RoomDevToolAction.MenuItemSend) + return true + } + return super.onOptionsItemSelected(item) + } + + override fun onBackPressed() { + viewModel.handle(RoomDevToolAction.OnBackPressed) + } + + private fun navigateTo(fragment: Fragment) { + val tag = fragment.javaClass.name + if (supportFragmentManager.findFragmentByTag(tag) == null) { + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) + .replace(R.id.container, fragment, tag) + .addToBackStack(tag) + .commit() + } else { + if (!supportFragmentManager.popBackStackImmediate(tag, 0)) { + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) + .replace(R.id.container, fragment, tag) + .addToBackStack(tag) + .commit() + } + } + } + + override fun onDestroy() { + supportFragmentManager.removeOnBackStackChangedListener(this) + currentDisplayMode = null + super.onDestroy() + } + + override fun onPrepareOptionsMenu(menu: Menu?): Boolean = withState(viewModel) { state -> + menu?.forEach { + val isVisible = when (it.itemId) { + R.id.menuItemEdit -> { + state.displayMode is RoomDevToolViewState.Mode.StateEventDetail + } + R.id.menuItemSend -> { + state.displayMode is RoomDevToolViewState.Mode.EditEventContent + || state.displayMode is RoomDevToolViewState.Mode.SendEventForm + } + else -> true + } + it.isVisible = isVisible + } + return@withState true + } + + companion object { + + fun intent(roomId: String, context: Context): Intent { + return Intent(context, RoomDevToolActivity::class.java).apply { + putExtra(MvRx.KEY_ARG, Args(roomId)) + } + } + } + + override fun onBackStackChanged() = withState(viewModel) { state -> + updateToolBar(state) + } + + private fun updateToolBar(state: RoomDevToolViewState) { + val title = when (state.displayMode) { + RoomDevToolViewState.Mode.Root -> { + getString(getTitleRes()) + } + RoomDevToolViewState.Mode.StateEventList -> { + "State Events" + } + RoomDevToolViewState.Mode.StateEventDetail -> { + state.selectedEvent?.type + } + RoomDevToolViewState.Mode.EditEventContent -> { + "Edit Content" + } + RoomDevToolViewState.Mode.StateEventListByType -> { + state.currentStateType ?: "" + } + is RoomDevToolViewState.Mode.SendEventForm -> { + if (state.displayMode.isState) "Send Custom State Event" + else "Send Custom Event" + } + } + + supportActionBar?.let { + it.title = title + } ?: run { + setTitle(title) + } + invalidateOptionsMenu() + } +} diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolEditFragment.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolEditFragment.kt new file mode 100644 index 0000000000..cbb85f5ee6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolEditFragment.kt @@ -0,0 +1,72 @@ +/* + * 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.devtools + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.ColorProvider +import im.vector.app.databinding.FragmentDevtoolsEditorBinding +import javax.inject.Inject + +class RoomDevToolEditFragment @Inject constructor( + val epoxyController: RoomDevToolRootController, + private val colorProvider: ColorProvider +) : VectorBaseFragment(), RoomDevToolRootController.InteractionListener { + + val sharedViewModel: RoomDevToolViewModel by activityViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentDevtoolsEditorBinding { + return FragmentDevtoolsEditorBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + withState(sharedViewModel) { + views.editText.setText(it.editedContent ?: "{}") + } + views.editText.textChanges() + .skipInitialValue() + .subscribe { + sharedViewModel.handle(RoomDevToolAction.UpdateContentText(it.toString())) + } + .disposeOnDestroyView() + } + + override fun invalidate() = withState(sharedViewModel) { _ -> + } + + override fun processAction(action: RoomDevToolAction) { + sharedViewModel.handle(action) + } + + override fun onResume() { + super.onResume() + views.editText.requestFocus() + } + + override fun onStop() { + super.onStop() + views.editText.hideKeyboard() + } +} diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolFragment.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolFragment.kt new file mode 100644 index 0000000000..36c1142bc2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolFragment.kt @@ -0,0 +1,72 @@ +/* + * 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.devtools + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.ColorProvider +import im.vector.app.databinding.FragmentGenericRecyclerBinding +import javax.inject.Inject + +class RoomDevToolFragment @Inject constructor( + val epoxyController: RoomDevToolRootController, + private val colorProvider: ColorProvider +) : VectorBaseFragment(), RoomDevToolRootController.InteractionListener { + + val sharedViewModel: RoomDevToolViewModel by activityViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding { + return FragmentGenericRecyclerBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.genericRecyclerView.configureWith(epoxyController, showDivider = true) + epoxyController.interactionListener = this + +// sharedViewModel.observeViewEvents { +// when (it) { +// is DevToolsViewEvents.showJson -> { +// JSonViewerDialog.newInstance(it.jsonString, -1, createJSonViewerStyleProvider(colorProvider)) +// .show(childFragmentManager, "JSON_VIEWER") +// +// } +// } +// } + } + + override fun onDestroyView() { + views.genericRecyclerView.cleanup() + epoxyController.interactionListener = null + super.onDestroyView() + } + + override fun invalidate() = withState(sharedViewModel) { state -> + epoxyController.setData(state) + } + + override fun processAction(action: RoomDevToolAction) { + sharedViewModel.handle(action) + } +} diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolRootController.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolRootController.kt new file mode 100644 index 0000000000..48f74eaaa5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolRootController.kt @@ -0,0 +1,60 @@ +/* + * 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.devtools + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericButtonItem +import javax.inject.Inject + +class RoomDevToolRootController @Inject constructor( + private val stringProvider: StringProvider +) : TypedEpoxyController() { + + interface InteractionListener { + fun processAction(action: RoomDevToolAction) + } + + var interactionListener: InteractionListener? = null + + override fun buildModels(data: RoomDevToolViewState?) { + if (data?.displayMode == RoomDevToolViewState.Mode.Root) { + genericButtonItem { + id("explore") + text("Explore Room State") + buttonClickAction(View.OnClickListener { + interactionListener?.processAction(RoomDevToolAction.ExploreRoomState) + }) + } + genericButtonItem { + id("send") + text("Send Custom Event") + buttonClickAction(View.OnClickListener { + interactionListener?.processAction(RoomDevToolAction.SendCustomEvent(false)) + }) + } + genericButtonItem { + id("send_state") + text("Send State Event") + buttonClickAction(View.OnClickListener { + interactionListener?.processAction(RoomDevToolAction.SendCustomEvent(true)) + }) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt new file mode 100644 index 0000000000..c6e9e37e9f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt @@ -0,0 +1,76 @@ +/* + * 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.devtools + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.features.form.formEditTextItem +import im.vector.app.features.form.formMultiLineEditTextItem +import javax.inject.Inject + +class RoomDevToolSendFormController @Inject constructor() : TypedEpoxyController() { + + interface InteractionListener { + fun processAction(action: RoomDevToolAction) + } + + var interactionListener: InteractionListener? = null + + override fun buildModels(data: RoomDevToolViewState?) { + val sendMode = (data?.displayMode as? RoomDevToolViewState.Mode.SendEventForm) + ?: return + + genericFooterItem { + id("topSpace") + text("") + } + formEditTextItem { + id("event_type") + enabled(true) + value(data.sendEventDraft?.type) + hint("Type") + showBottomSeparator(false) + onTextChange { text -> + interactionListener?.processAction(RoomDevToolAction.CustomEventTypeChange(text)) + } + } + + if (sendMode.isState) { + formEditTextItem { + id("state_key") + enabled(true) + value(data.sendEventDraft?.stateKey) + hint("State Key") + showBottomSeparator(false) + onTextChange { text -> + interactionListener?.processAction(RoomDevToolAction.CustomEventStateKeyChange(text)) + } + } + } + + formMultiLineEditTextItem { + id("event_content") + enabled(true) + value(data.sendEventDraft?.content) + hint("Event Content") + showBottomSeparator(false) + onTextChange { text -> + interactionListener?.processAction(RoomDevToolAction.CustomEventContentChange(text)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormFragment.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormFragment.kt new file mode 100644 index 0000000000..d9e6f911c9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormFragment.kt @@ -0,0 +1,62 @@ +/* + * 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.devtools + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.ColorProvider +import im.vector.app.databinding.FragmentGenericRecyclerBinding +import javax.inject.Inject + +class RoomDevToolSendFormFragment @Inject constructor( + val epoxyController: RoomDevToolSendFormController, + private val colorProvider: ColorProvider +) : VectorBaseFragment(), RoomDevToolSendFormController.InteractionListener { + + val sharedViewModel: RoomDevToolViewModel by activityViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding { + return FragmentGenericRecyclerBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.genericRecyclerView.configureWith(epoxyController, showDivider = false) + epoxyController.interactionListener = this + } + + override fun onDestroyView() { + views.genericRecyclerView.cleanup() + epoxyController.interactionListener = null + super.onDestroyView() + } + + override fun invalidate() = withState(sharedViewModel) { state -> + epoxyController.setData(state) + } + + override fun processAction(action: RoomDevToolAction) { + sharedViewModel.handle(action) + } +} diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolStateEventListFragment.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolStateEventListFragment.kt new file mode 100644 index 0000000000..f2425b9713 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolStateEventListFragment.kt @@ -0,0 +1,62 @@ +/* + * 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.devtools + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.ColorProvider +import im.vector.app.databinding.FragmentGenericRecyclerBinding +import javax.inject.Inject + +class RoomDevToolStateEventListFragment @Inject constructor( + val epoxyController: RoomStateListController, + private val colorProvider: ColorProvider +) : VectorBaseFragment(), RoomStateListController.InteractionListener { + + val sharedViewModel: RoomDevToolViewModel by activityViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding { + return FragmentGenericRecyclerBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.genericRecyclerView.configureWith(epoxyController, showDivider = true) + epoxyController.interactionListener = this + } + + override fun onDestroyView() { + views.genericRecyclerView.cleanup() + epoxyController.interactionListener = null + super.onDestroyView() + } + + override fun invalidate() = withState(sharedViewModel) { state -> + epoxyController.setData(state) + } + + override fun processAction(action: RoomDevToolAction) { + sharedViewModel.handle(action) + } +} diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt new file mode 100644 index 0000000000..7c847be3b1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt @@ -0,0 +1,293 @@ +/* + * 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.devtools + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.ViewModelContext +import com.squareup.moshi.Types +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.launch +import org.json.JSONObject +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.rx.rx + +class RoomDevToolViewModel @AssistedInject constructor( + @Assisted val initialState: RoomDevToolViewState, + private val errorFormatter: ErrorFormatter, + private val session: Session +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory { + fun create(initialState: RoomDevToolViewState): RoomDevToolViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomDevToolViewState): RoomDevToolViewModel { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + init { + + session.getRoom(initialState.roomId)?.rx() + ?.liveStateEvents(emptySet()) + ?.execute { async -> + copy(stateEvents = async) + } + } + + override fun handle(action: RoomDevToolAction) { + when (action) { + RoomDevToolAction.ExploreRoomState -> { + setState { + copy( + displayMode = RoomDevToolViewState.Mode.StateEventList, + selectedEvent = null + ) + } + } + is RoomDevToolAction.ShowStateEvent -> { + val jsonString = MoshiProvider.providesMoshi() + .adapter(Event::class.java) + .toJson(action.event) + + setState { + copy( + displayMode = RoomDevToolViewState.Mode.StateEventDetail, + selectedEvent = action.event, + selectedEventJson = jsonString + ) + } + } + RoomDevToolAction.OnBackPressed -> { + handleBack() + } + RoomDevToolAction.MenuEdit -> { + withState { + if (it.displayMode == RoomDevToolViewState.Mode.StateEventDetail) { + // we want to edit it + val content = it.selectedEvent?.content?.let { JSONObject(it).toString(4) } ?: "{\n\t\n}" + setState { + copy( + editedContent = content, + displayMode = RoomDevToolViewState.Mode.EditEventContent + ) + } + } + } + } + is RoomDevToolAction.ShowStateEventType -> { + setState { + copy( + displayMode = RoomDevToolViewState.Mode.StateEventListByType, + currentStateType = action.stateEventType + ) + } + } + RoomDevToolAction.MenuItemSend -> { + handleMenuItemSend() + } + is RoomDevToolAction.UpdateContentText -> { + setState { + copy(editedContent = action.contentJson) + } + } + is RoomDevToolAction.SendCustomEvent -> { + setState { + copy( + displayMode = RoomDevToolViewState.Mode.SendEventForm(action.isStateEvent), + sendEventDraft = RoomDevToolViewState.SendEventDraft("m.room.message", null, "{\n}") + ) + } + } + is RoomDevToolAction.CustomEventTypeChange -> { + setState { + copy( + sendEventDraft = sendEventDraft?.copy(type = action.type) + ) + } + } + is RoomDevToolAction.CustomEventStateKeyChange -> { + setState { + copy( + sendEventDraft = sendEventDraft?.copy(stateKey = action.stateKey) + ) + } + } + is RoomDevToolAction.CustomEventContentChange -> { + setState { + copy( + sendEventDraft = sendEventDraft?.copy(content = action.content) + ) + } + } + } + } + + private fun handleMenuItemSend() = withState { + when (it.displayMode) { + RoomDevToolViewState.Mode.EditEventContent -> { + setState { copy(modalLoading = Loading()) } + viewModelScope.launch { + try { + val room = session.getRoom(initialState.roomId) + ?: throw IllegalArgumentException("Room not found") + + val adapter = MoshiProvider.providesMoshi() + .adapter(Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java)) + val json = adapter.fromJson(it.editedContent ?: "") + ?: throw IllegalArgumentException("No content") + + room.sendStateEvent( + it.selectedEvent?.type ?: "", + it.selectedEvent?.stateKey, + json + + ) + _viewEvents.post(DevToolsViewEvents.showSnackMessage("State event sent!")) + setState { + copy( + modalLoading = Success(Unit), + selectedEventJson = null, + editedContent = null, + displayMode = RoomDevToolViewState.Mode.StateEventListByType + ) + } + } catch (failure: Throwable) { + _viewEvents.post(DevToolsViewEvents.showAlertMessage(errorFormatter.toHumanReadable(failure))) + setState { copy(modalLoading = Fail(failure)) } + } + } + } + is RoomDevToolViewState.Mode.SendEventForm -> { + setState { copy(modalLoading = Loading()) } + viewModelScope.launch { + try { + val room = session.getRoom(initialState.roomId) + ?: throw IllegalArgumentException("Room not found") + + val adapter = MoshiProvider.providesMoshi() + .adapter(Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java)) + val json = adapter.fromJson(it.sendEventDraft?.content ?: "") + ?: throw IllegalArgumentException("No content") + + val eventType = it.sendEventDraft?.type ?: throw IllegalArgumentException("Missing message type") + if (it.displayMode.isState) { + room.sendStateEvent( + eventType, + it.sendEventDraft.stateKey, + json + + ) + } else { + // can we try to do some validation?? + // val validParse = MoshiProvider.providesMoshi().adapter(MessageContent::class.java).fromJson(it.sendEventDraft.content ?: "") + json.toModel(catchError = false) + ?: throw IllegalArgumentException("Malformed event") + room.sendEvent( + eventType, + json + ) + } + + _viewEvents.post(DevToolsViewEvents.showSnackMessage("Event sent!")) + setState { + copy( + modalLoading = Success(Unit), + sendEventDraft = null, + displayMode = RoomDevToolViewState.Mode.Root + ) + } + } catch (failure: Throwable) { + _viewEvents.post(DevToolsViewEvents.showAlertMessage(errorFormatter.toHumanReadable(failure))) + setState { copy(modalLoading = Fail(failure)) } + } + } + } + } + } + + private fun handleBack() = withState { + when (it.displayMode) { + RoomDevToolViewState.Mode.Root -> { + _viewEvents.post(DevToolsViewEvents.Dismiss) + } + RoomDevToolViewState.Mode.StateEventList -> { + setState { + copy( + selectedEvent = null, + selectedEventJson = null, + displayMode = RoomDevToolViewState.Mode.Root + ) + } + } + RoomDevToolViewState.Mode.StateEventDetail -> { + setState { + copy( + selectedEvent = null, + selectedEventJson = null, + displayMode = RoomDevToolViewState.Mode.StateEventListByType + ) + } + } + RoomDevToolViewState.Mode.EditEventContent -> { + setState { + copy( + displayMode = RoomDevToolViewState.Mode.StateEventDetail + ) + } + } + RoomDevToolViewState.Mode.StateEventListByType -> { + setState { + copy( + currentStateType = null, + displayMode = RoomDevToolViewState.Mode.StateEventList + ) + } + } + is RoomDevToolViewState.Mode.SendEventForm -> { + setState { + copy( + displayMode = RoomDevToolViewState.Mode.Root + ) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewState.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewState.kt new file mode 100644 index 0000000000..885de005b0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewState.kt @@ -0,0 +1,52 @@ +/* + * 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.devtools + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.api.session.events.model.Event + +data class RoomDevToolViewState( + val roomId: String = "", + val displayMode: Mode = Mode.Root, + val stateEvents: Async> = Uninitialized, + val currentStateType: String? = null, + val selectedEvent: Event? = null, + val selectedEventJson: String? = null, + val editedContent: String? = null, + val modalLoading: Async = Uninitialized, + val sendEventDraft: SendEventDraft? = null +) : MvRxState { + + constructor(args: RoomDevToolActivity.Args) : this(roomId = args.roomId, displayMode = Mode.Root) + + sealed class Mode { + object Root : Mode() + object StateEventList : Mode() + object StateEventListByType : Mode() + object StateEventDetail : Mode() + object EditEventContent : Mode() + data class SendEventForm(val isState: Boolean) : Mode() + } + + data class SendEventDraft( + val type: String?, + val stateKey: String?, + val content: String? + ) +} diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomStateListController.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomStateListController.kt new file mode 100644 index 0000000000..844ed9be20 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomStateListController.kt @@ -0,0 +1,113 @@ +/* + * 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.devtools + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R +import im.vector.app.core.epoxy.noResultItem +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.GenericItem +import im.vector.app.core.ui.list.genericItem +import me.gujun.android.span.span +import org.json.JSONObject +import javax.inject.Inject + +class RoomStateListController @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider +) : TypedEpoxyController() { + + interface InteractionListener { + fun processAction(action: RoomDevToolAction) + } + + var interactionListener: InteractionListener? = null + + override fun buildModels(data: RoomDevToolViewState?) { + when (data?.displayMode) { + RoomDevToolViewState.Mode.StateEventList -> { + val stateEventsGroups = (data.stateEvents.invoke() ?: emptyList()).groupBy { + it.type + } + + if (stateEventsGroups.isEmpty()) { + noResultItem { + id("no state events") + text(stringProvider.getString(R.string.no_result_placeholder)) + } + } else { + stateEventsGroups.forEach { entry -> + genericItem { + id(entry.key) + title(entry.key) + description("${entry.value.size} entries") + itemClickAction(GenericItem.Action("view").apply { + perform = Runnable { + interactionListener?.processAction(RoomDevToolAction.ShowStateEventType(entry.key)) + } + }) + } + } + } + } + RoomDevToolViewState.Mode.StateEventListByType -> { + val stateEvents = (data.stateEvents.invoke() ?: emptyList()).filter { it.type == data.currentStateType } + if (stateEvents.isEmpty()) { + noResultItem { + id("no state events") + text(stringProvider.getString(R.string.no_result_placeholder)) + } + } else { + stateEvents.forEach { stateEvent -> + val contentMap = JSONObject(stateEvent.content ?: emptyMap()).toString().let { + if (it.length > 140) { + it.take(140) + Typography.ellipsis + } else it.take(140) + } + genericItem { + id(stateEvent.eventId) + title(span { + +"Type: " + span { + textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + text = "\"${stateEvent.type}\"" + textStyle = "normal" + } + +"\nState Key: " + span { + textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + text = stateEvent.stateKey.let { "\"$it\"" } + textStyle = "normal" + } + }) + description(contentMap) + itemClickAction(GenericItem.Action("view").apply { + perform = Runnable { + interactionListener?.processAction(RoomDevToolAction.ShowStateEvent(stateEvent)) + } + }) + } + } + } + } + else -> { + // nop + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/form/FormMultiLineEditTextItem.kt b/vector/src/main/java/im/vector/app/features/form/FormMultiLineEditTextItem.kt new file mode 100644 index 0000000000..974d2594ee --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/form/FormMultiLineEditTextItem.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2019 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.form + +import android.graphics.Typeface +import android.text.Editable +import android.view.View +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextSafe +import im.vector.app.core.platform.SimpleTextWatcher + +@EpoxyModelClass(layout = R.layout.item_form_multiline_text_input) +abstract class FormMultiLineEditTextItem : VectorEpoxyModel() { + + @EpoxyAttribute + var hint: String? = null + + @EpoxyAttribute + var value: String? = null + + @EpoxyAttribute + var showBottomSeparator: Boolean = true + + @EpoxyAttribute + var errorMessage: String? = null + + @EpoxyAttribute + var enabled: Boolean = true + + @EpoxyAttribute + var textSizeSp: Int? = null + + @EpoxyAttribute + var minLines: Int = 3 + + @EpoxyAttribute + var typeFace: Typeface = Typeface.DEFAULT + + @EpoxyAttribute + var onTextChange: ((String) -> Unit)? = null + + private val onTextChangeListener = object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + onTextChange?.invoke(s.toString()) + } + } + + override fun bind(holder: Holder) { + super.bind(holder) + holder.textInputLayout.isEnabled = enabled + holder.textInputLayout.hint = hint + holder.textInputLayout.error = errorMessage + + holder.textInputEditText.typeface = typeFace + holder.textInputEditText.textSize = textSizeSp?.toFloat() ?: 12f + holder.textInputEditText.minLines = minLines + + // Update only if text is different and value is not null + holder.textInputEditText.setTextSafe(value) + holder.textInputEditText.isEnabled = enabled + + holder.textInputEditText.addTextChangedListener(onTextChangeListener) + holder.bottomSeparator.isVisible = showBottomSeparator + } + + override fun shouldSaveViewState(): Boolean { + return false + } + + override fun unbind(holder: Holder) { + super.unbind(holder) + holder.textInputEditText.removeTextChangedListener(onTextChangeListener) + } + + class Holder : VectorEpoxyHolder() { + val textInputLayout by bind(R.id.formMultiLineTextInputLayout) + val textInputEditText by bind(R.id.formMultiLineEditText) + val bottomSeparator by bind(R.id.formTextInputDivider) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index aeb1c30f4b..8347735d0a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -128,6 +128,7 @@ import im.vector.app.features.command.Command import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.util.toImageRes import im.vector.app.features.crypto.verification.VerificationBottomSheet +import im.vector.app.features.devtools.RoomDevToolActivity import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.composer.TextComposerView import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet @@ -767,6 +768,10 @@ class RoomDetailFragment @Inject constructor( handleSearchAction() true } + R.id.dev_tools -> { + startActivity(RoomDevToolActivity.intent(roomDetailArgs.roomId, requireContext())) + true + } else -> super.onOptionsItemSelected(item) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 7eedd5ca8e..f7299363a3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -606,6 +606,7 @@ class RoomDetailViewModel @AssistedInject constructor( R.id.video_call -> true // always show for discoverability R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null R.id.search -> true + R.id.dev_tools -> vectorPreferences.developerMode() else -> false } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt index 71ac7fcec4..86146e7d3a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt @@ -63,7 +63,8 @@ class RoomMemberListController @Inject constructor( ?.filter { event -> event.content.toModel() ?.takeIf { - data.filter.isEmpty() || it.displayName.contains(data.filter, ignoreCase = true) + it.displayName != null + && (data.filter.isEmpty() || it.displayName!!.contains(data.filter, ignoreCase = true)) } != null } .orEmpty() diff --git a/vector/src/main/res/layout/fragment_devtools_editor.xml b/vector/src/main/res/layout/fragment_devtools_editor.xml new file mode 100644 index 0000000000..98682a1b65 --- /dev/null +++ b/vector/src/main/res/layout/fragment_devtools_editor.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_form_multiline_text_input.xml b/vector/src/main/res/layout/item_form_multiline_text_input.xml new file mode 100644 index 0000000000..f844f91a5a --- /dev/null +++ b/vector/src/main/res/layout/item_form_multiline_text_input.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_generic_list.xml b/vector/src/main/res/layout/item_generic_list.xml index f89413f15f..5535ab4549 100644 --- a/vector/src/main/res/layout/item_generic_list.xml +++ b/vector/src/main/res/layout/item_generic_list.xml @@ -5,6 +5,7 @@ android:id="@+id/item_generic_root" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" android:minHeight="50dp"> + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml index 72b4a00682..1f4e2736b1 100644 --- a/vector/src/main/res/menu/menu_timeline.xml +++ b/vector/src/main/res/menu/menu_timeline.xml @@ -68,4 +68,12 @@ app:showAsAction="never" tools:visible="true" /> + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index cfcbd157f3..c2677bf89e 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2792,4 +2792,5 @@ Re-Authentication Needed Element requires you to enter your credentials to perform this action. Failed to authenticate + Dev Tools From 3c0c445ea7b7d0114b5f2abd508c28693b91847d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 12 Feb 2021 14:35:22 +0100 Subject: [PATCH 02/62] Fix issue on Slovanian language --- vector/src/main/res/values-sl/strings_no_weblate.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/res/values-sl/strings_no_weblate.xml b/vector/src/main/res/values-sl/strings_no_weblate.xml index fe3f5085d5..781cf2f4a4 100644 --- a/vector/src/main/res/values-sl/strings_no_weblate.xml +++ b/vector/src/main/res/values-sl/strings_no_weblate.xml @@ -1,6 +1,7 @@ + sl SI \ No newline at end of file From ebd55ea2828ca9f3ef6882c2f172ac8ad6f2e29a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 12 Feb 2021 14:35:28 +0100 Subject: [PATCH 03/62] Fix some lint issue about accessibility --- vector/lint.xml | 3 +++ vector/src/main/res/layout/dialog_base_edit_text.xml | 1 + vector/src/main/res/layout/dialog_preference_edit_text.xml | 1 + vector/src/main/res/layout/fragment_login_server_url_form.xml | 1 + vector/src/main/res/values/strings.xml | 2 ++ 5 files changed, 8 insertions(+) diff --git a/vector/lint.xml b/vector/lint.xml index 572f937406..2ec7f44d7a 100644 --- a/vector/lint.xml +++ b/vector/lint.xml @@ -17,6 +17,9 @@ + + + diff --git a/vector/src/main/res/layout/dialog_base_edit_text.xml b/vector/src/main/res/layout/dialog_base_edit_text.xml index 1c4695a815..5c3255060c 100644 --- a/vector/src/main/res/layout/dialog_base_edit_text.xml +++ b/vector/src/main/res/layout/dialog_base_edit_text.xml @@ -12,6 +12,7 @@ android:id="@+id/editText" android:layout_width="match_parent" android:layout_height="wrap_content" + android:hint="@string/dialog_edit_hint" android:inputType="text"> diff --git a/vector/src/main/res/layout/dialog_preference_edit_text.xml b/vector/src/main/res/layout/dialog_preference_edit_text.xml index a6c570f751..3933231a34 100644 --- a/vector/src/main/res/layout/dialog_preference_edit_text.xml +++ b/vector/src/main/res/layout/dialog_preference_edit_text.xml @@ -20,6 +20,7 @@ android:id="@android:id/edit" android:layout_width="match_parent" android:layout_height="wrap_content" + android:hint="@string/dialog_edit_hint" android:paddingStart="4dp" android:paddingEnd="4dp" tools:ignore="TextFields" /> diff --git a/vector/src/main/res/layout/fragment_login_server_url_form.xml b/vector/src/main/res/layout/fragment_login_server_url_form.xml index a441fee3be..d3b69043da 100644 --- a/vector/src/main/res/layout/fragment_login_server_url_form.xml +++ b/vector/src/main/res/layout/fragment_login_server_url_form.xml @@ -64,6 +64,7 @@ android:id="@+id/loginServerUrlFormHomeServerUrl" android:layout_width="match_parent" android:layout_height="wrap_content" + android:hint="@string/hs_url" android:imeOptions="actionDone" android:inputType="textUri" android:maxLines="1" /> diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 3fbd233888..e235e5eb70 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -144,6 +144,8 @@ Error Success + New value + Home Notifications From 9e86b35f8cbe923332a79e13c328876d6a43a24e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 12 Feb 2021 15:04:03 +0100 Subject: [PATCH 04/62] Fix some lint issue about accessibility --- .../java/im/vector/app/core/dialogs/ExportKeysDialog.kt | 2 +- vector/src/main/res/layout/activity_bug_report.xml | 1 + vector/src/main/res/layout/activity_call.xml | 6 +++++- .../src/main/res/layout/bottom_sheet_call_dial_pad.xml | 9 ++++++--- .../main/res/layout/bottom_sheet_logout_and_backup.xml | 1 + .../src/main/res/layout/bottom_sheet_matrix_to_card.xml | 1 + .../res/layout/bottom_sheet_room_widget_permission.xml | 1 + .../main/res/layout/bottom_sheet_save_recovery_key.xml | 2 ++ .../main/res/layout/custom_action_item_layout_badge.xml | 2 ++ vector/src/main/res/layout/dialog_change_password.xml | 1 + vector/src/main/res/layout/dialog_export_e2e_keys.xml | 1 + vector/src/main/res/layout/dialog_import_e2e_keys.xml | 1 + vector/src/main/res/layout/dialog_prompt_password.xml | 2 +- .../src/main/res/layout/fragment_attachments_preview.xml | 1 + .../layout/fragment_bootstrap_enter_account_password.xml | 1 + .../res/layout/fragment_bootstrap_migrate_backup.xml | 1 + vector/src/main/res/layout/fragment_contacts_book.xml | 1 + .../fragment_create_direct_room_directory_users.xml | 1 + vector/src/main/res/layout/fragment_create_room.xml | 1 + vector/src/main/res/layout/fragment_home_drawer.xml | 2 ++ .../res/layout/fragment_keys_backup_restore_from_key.xml | 2 ++ .../fragment_keys_backup_restore_from_passphrase.xml | 2 ++ vector/src/main/res/values/strings.xml | 4 ++++ 23 files changed, 40 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/dialogs/ExportKeysDialog.kt b/vector/src/main/java/im/vector/app/core/dialogs/ExportKeysDialog.kt index e137eb1b70..6e7d2a3f4d 100644 --- a/vector/src/main/java/im/vector/app/core/dialogs/ExportKeysDialog.kt +++ b/vector/src/main/java/im/vector/app/core/dialogs/ExportKeysDialog.kt @@ -61,7 +61,7 @@ class ExportKeysDialog { passwordVisible = !passwordVisible views.exportDialogEt.showPassword(passwordVisible) views.exportDialogEtConfirm.showPassword(passwordVisible) - views.exportDialogShowPassword.setImageResource(if (passwordVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye) + views.exportDialogShowPassword.setImageResource(if (passwordVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye) // TODO Content description } val exportDialog = builder.show() diff --git a/vector/src/main/res/layout/activity_bug_report.xml b/vector/src/main/res/layout/activity_bug_report.xml index 34169f44f8..2347d84ee3 100644 --- a/vector/src/main/res/layout/activity_bug_report.xml +++ b/vector/src/main/res/layout/activity_bug_report.xml @@ -150,6 +150,7 @@ android:layout_gravity="center_horizontal" android:layout_marginTop="10dp" android:adjustViewBounds="true" + android:contentDescription="@string/a11y_screenshot" android:maxWidth="260dp" android:scaleType="fitCenter" tools:src="@tools:sample/backgrounds/scenic" /> diff --git a/vector/src/main/res/layout/activity_call.xml b/vector/src/main/res/layout/activity_call.xml index ff33c5f17c..7ea632eefb 100644 --- a/vector/src/main/res/layout/activity_call.xml +++ b/vector/src/main/res/layout/activity_call.xml @@ -16,6 +16,7 @@ android:id="@+id/bgCallView" android:layout_width="match_parent" android:layout_height="match_parent" + android:importantForAccessibility="no" android:scaleType="centerCrop" tools:src="@tools:sample/avatars" /> @@ -53,6 +54,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:foreground="?attr/selectableItemBackground" + android:importantForAccessibility="no" android:scaleType="centerCrop" tools:src="@tools:sample/avatars" /> @@ -61,6 +63,7 @@ android:layout_width="20dp" android:layout_height="20dp" android:layout_gravity="center" + android:importantForAccessibility="no" android:src="@drawable/ic_call_small_pause" /> @@ -70,6 +73,7 @@ android:layout_width="80dp" android:layout_height="80dp" android:contentDescription="@string/avatar" + android:importantForAccessibility="no" android:scaleType="centerCrop" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -81,13 +85,13 @@ android:id="@+id/smallIsHeldIcon" android:layout_width="20dp" android:layout_height="20dp" + android:importantForAccessibility="no" android:src="@drawable/ic_call_small_pause" app:layout_constraintBottom_toBottomOf="@id/otherMemberAvatar" app:layout_constraintEnd_toEndOf="@id/otherMemberAvatar" app:layout_constraintStart_toStartOf="@id/otherMemberAvatar" app:layout_constraintTop_toTopOf="@id/otherMemberAvatar" /> - + android:scaleType="center" + android:src="@drawable/ic_cross" + app:tint="?riotx_text_primary" + tools:ignore="MissingPrefix" /> diff --git a/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml b/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml index 97168b4bdb..069f52df31 100644 --- a/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml +++ b/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml @@ -47,6 +47,7 @@ android:id="@+id/backupCompleteImage" android:layout_width="20dp" android:layout_height="20dp" + android:importantForAccessibility="no" android:visibility="gone" app:srcCompat="@drawable/unit_test_ok" tools:visibility="visible" /> diff --git a/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml b/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml index d051bd7c98..37f9633728 100644 --- a/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml +++ b/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml @@ -22,6 +22,7 @@ android:layout_width="60dp" android:layout_height="60dp" android:layout_marginTop="@dimen/layout_vertical_margin_big" + android:contentDescription="@string/avatar" android:elevation="4dp" android:transitionName="profile" app:layout_constraintEnd_toEndOf="parent" diff --git a/vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml b/vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml index 56459e90d6..2f8d890131 100644 --- a/vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml +++ b/vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml @@ -45,6 +45,7 @@ android:layout_width="40dp" android:layout_height="40dp" android:layout_gravity="center" + android:contentDescription="@string/avatar" tools:src="@tools:sample/avatars" /> @@ -77,6 +78,7 @@ android:layout_height="24dp" android:layout_gravity="center_vertical" android:layout_marginEnd="16dp" + android:importantForAccessibility="no" android:src="@drawable/ic_material_save" app:tint="?colorAccent" tools:ignore="MissingPrefix" /> diff --git a/vector/src/main/res/layout/custom_action_item_layout_badge.xml b/vector/src/main/res/layout/custom_action_item_layout_badge.xml index 4d1d398559..b9b3e7fef2 100644 --- a/vector/src/main/res/layout/custom_action_item_layout_badge.xml +++ b/vector/src/main/res/layout/custom_action_item_layout_badge.xml @@ -6,12 +6,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:clipToPadding="false" + android:contentDescription="@string/a11y_open_widget" android:focusable="true"> diff --git a/vector/src/main/res/layout/fragment_attachments_preview.xml b/vector/src/main/res/layout/fragment_attachments_preview.xml index ec99cce9b3..f40cc51e9d 100644 --- a/vector/src/main/res/layout/fragment_attachments_preview.xml +++ b/vector/src/main/res/layout/fragment_attachments_preview.xml @@ -67,6 +67,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" + android:contentDescription="@string/send" android:src="@drawable/ic_send" app:layout_constraintBottom_toTopOf="@id/attachmentPreviewerBottomContainer" app:layout_constraintEnd_toEndOf="parent" diff --git a/vector/src/main/res/layout/fragment_bootstrap_enter_account_password.xml b/vector/src/main/res/layout/fragment_bootstrap_enter_account_password.xml index cc1e809a6c..d4f4e044ce 100644 --- a/vector/src/main/res/layout/fragment_bootstrap_enter_account_password.xml +++ b/vector/src/main/res/layout/fragment_bootstrap_enter_account_password.xml @@ -49,6 +49,7 @@ android:layout_height="@dimen/layout_touch_size" android:layout_marginTop="8dp" android:background="?attr/selectableItemBackground" + android:contentDescription="@string/a11y_show_password" android:scaleType="center" android:src="@drawable/ic_eye" app:layout_constraintEnd_toEndOf="parent" diff --git a/vector/src/main/res/layout/fragment_bootstrap_migrate_backup.xml b/vector/src/main/res/layout/fragment_bootstrap_migrate_backup.xml index 770fc405a7..d05aad5101 100644 --- a/vector/src/main/res/layout/fragment_bootstrap_migrate_backup.xml +++ b/vector/src/main/res/layout/fragment_bootstrap_migrate_backup.xml @@ -69,6 +69,7 @@ android:layout_height="@dimen/layout_touch_size" android:layout_marginTop="8dp" android:background="?attr/selectableItemBackground" + android:contentDescription="@string/a11y_show_password" android:scaleType="center" android:src="@drawable/ic_eye" app:layout_constraintEnd_toEndOf="parent" diff --git a/vector/src/main/res/layout/fragment_contacts_book.xml b/vector/src/main/res/layout/fragment_contacts_book.xml index 843b15e9f2..0ff64cf657 100644 --- a/vector/src/main/res/layout/fragment_contacts_book.xml +++ b/vector/src/main/res/layout/fragment_contacts_book.xml @@ -29,6 +29,7 @@ android:layout_width="@dimen/layout_touch_size" android:layout_height="@dimen/layout_touch_size" android:clickable="true" + android:contentDescription="@string/action_close" android:focusable="true" android:foreground="?attr/selectableItemBackground" android:scaleType="center" diff --git a/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml b/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml index 6a18bdea0b..69e9e658cb 100644 --- a/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml +++ b/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml @@ -29,6 +29,7 @@ android:layout_width="@dimen/layout_touch_size" android:layout_height="@dimen/layout_touch_size" android:clickable="true" + android:contentDescription="@string/action_close" android:focusable="true" android:foreground="?attr/selectableItemBackground" android:scaleType="center" diff --git a/vector/src/main/res/layout/fragment_create_room.xml b/vector/src/main/res/layout/fragment_create_room.xml index 015b739dc9..89883fb8b4 100644 --- a/vector/src/main/res/layout/fragment_create_room.xml +++ b/vector/src/main/res/layout/fragment_create_room.xml @@ -29,6 +29,7 @@ android:id="@+id/createRoomClose" android:layout_width="@dimen/layout_touch_size" android:layout_height="@dimen/layout_touch_size" + android:contentDescription="@string/action_close" android:scaleType="center" android:src="@drawable/ic_x_18dp" app:layout_constraintBottom_toBottomOf="parent" diff --git a/vector/src/main/res/layout/fragment_home_drawer.xml b/vector/src/main/res/layout/fragment_home_drawer.xml index e627882d96..642119e5f8 100644 --- a/vector/src/main/res/layout/fragment_home_drawer.xml +++ b/vector/src/main/res/layout/fragment_home_drawer.xml @@ -22,6 +22,7 @@ style="@style/VectorDebug" android:layout_width="@dimen/layout_touch_size" android:layout_height="@dimen/layout_touch_size" + android:importantForAccessibility="no" android:scaleType="center" android:src="@drawable/ic_settings_x" app:layout_constraintEnd_toEndOf="parent" @@ -35,6 +36,7 @@ android:layout_height="50dp" android:layout_marginStart="@dimen/layout_horizontal_margin" android:layout_marginTop="24dp" + android:contentDescription="@string/avatar" android:transitionName="profile" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/vector/src/main/res/layout/fragment_keys_backup_restore_from_key.xml b/vector/src/main/res/layout/fragment_keys_backup_restore_from_key.xml index 96401470ac..4920f685db 100644 --- a/vector/src/main/res/layout/fragment_keys_backup_restore_from_key.xml +++ b/vector/src/main/res/layout/fragment_keys_backup_restore_from_key.xml @@ -16,6 +16,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="36dp" + android:importantForAccessibility="no" android:src="@drawable/key_big" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -67,6 +68,7 @@ android:layout_width="48dp" android:layout_height="48dp" android:background="?attr/selectableItemBackground" + android:contentDescription="@string/a11y_import_key_from_file" android:scaleType="center" android:src="@drawable/ic_import_black" app:layout_constraintEnd_toEndOf="parent" diff --git a/vector/src/main/res/layout/fragment_keys_backup_restore_from_passphrase.xml b/vector/src/main/res/layout/fragment_keys_backup_restore_from_passphrase.xml index b3a243d9c6..817c2e15a7 100644 --- a/vector/src/main/res/layout/fragment_keys_backup_restore_from_passphrase.xml +++ b/vector/src/main/res/layout/fragment_keys_backup_restore_from_passphrase.xml @@ -16,6 +16,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="36dp" + android:importantForAccessibility="no" android:src="@drawable/key_big" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -67,6 +68,7 @@ android:layout_height="@dimen/layout_touch_size" android:layout_marginTop="8dp" android:background="?attr/selectableItemBackground" + android:contentDescription="@string/a11y_show_password" android:scaleType="center" android:src="@drawable/ic_eye" app:layout_constraintEnd_toEndOf="parent" diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index e235e5eb70..38f2001f6d 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2829,4 +2829,8 @@ Re-Authentication Needed Element requires you to enter your credentials to perform this action. Failed to authenticate + + Screenshot + Open widgets + Import key from file From 15f479b42420a1e2ca6c0fdc1498a99ad0a0ad80 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 12 Feb 2021 15:19:38 +0100 Subject: [PATCH 05/62] Create a View for password reveal button -> better management of a11y --- .../app/core/dialogs/ExportKeysDialog.kt | 2 +- .../app/core/dialogs/PromptPasswordDialog.kt | 2 +- .../core/ui/views/RevealPasswordImageView.kt | 43 +++++++++++++++++++ .../app/features/auth/PromptFragment.kt | 9 +--- ...KeysBackupRestoreFromPassphraseFragment.kt | 2 +- .../setup/KeysBackupSetupStep2Fragment.kt | 2 +- .../SharedSecuredStoragePassphraseFragment.kt | 2 +- .../BootstrapConfirmPassphraseFragment.kt | 2 +- .../BootstrapEnterPassphraseFragment.kt | 2 +- .../recover/BootstrapMigrateBackupFragment.kt | 2 +- .../app/features/login/LoginFragment.kt | 9 +--- .../login/LoginResetPasswordFragment.kt | 9 +--- .../settings/VectorSettingsGeneralFragment.kt | 2 +- .../VectorSettingsSecurityPrivacyFragment.kt | 2 +- .../soft/epoxy/LoginPasswordFormItem.kt | 13 ++---- .../res/layout/dialog_change_password.xml | 2 +- .../res/layout/dialog_export_e2e_keys.xml | 2 +- .../res/layout/dialog_import_e2e_keys.xml | 2 +- .../res/layout/dialog_prompt_password.xml | 2 +- .../fragment_bootstrap_enter_passphrase.xml | 3 +- .../fragment_bootstrap_migrate_backup.xml | 2 +- ...nt_keys_backup_restore_from_passphrase.xml | 2 +- .../fragment_keys_backup_setup_step2.xml | 2 +- vector/src/main/res/layout/fragment_login.xml | 4 +- .../layout/fragment_login_reset_password.xml | 4 +- .../res/layout/fragment_reauth_confirm.xml | 4 +- .../fragment_ssss_access_from_passphrase.xml | 3 +- .../res/layout/item_login_password_form.xml | 4 +- 28 files changed, 78 insertions(+), 61 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/ui/views/RevealPasswordImageView.kt diff --git a/vector/src/main/java/im/vector/app/core/dialogs/ExportKeysDialog.kt b/vector/src/main/java/im/vector/app/core/dialogs/ExportKeysDialog.kt index 6e7d2a3f4d..23018fe758 100644 --- a/vector/src/main/java/im/vector/app/core/dialogs/ExportKeysDialog.kt +++ b/vector/src/main/java/im/vector/app/core/dialogs/ExportKeysDialog.kt @@ -61,7 +61,7 @@ class ExportKeysDialog { passwordVisible = !passwordVisible views.exportDialogEt.showPassword(passwordVisible) views.exportDialogEtConfirm.showPassword(passwordVisible) - views.exportDialogShowPassword.setImageResource(if (passwordVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye) // TODO Content description + views.exportDialogShowPassword.render(passwordVisible) } val exportDialog = builder.show() diff --git a/vector/src/main/java/im/vector/app/core/dialogs/PromptPasswordDialog.kt b/vector/src/main/java/im/vector/app/core/dialogs/PromptPasswordDialog.kt index 6d7b721976..1839a8b11c 100644 --- a/vector/src/main/java/im/vector/app/core/dialogs/PromptPasswordDialog.kt +++ b/vector/src/main/java/im/vector/app/core/dialogs/PromptPasswordDialog.kt @@ -44,7 +44,7 @@ class PromptPasswordDialog { views.promptPasswordPasswordReveal.setOnClickListener { passwordVisible = !passwordVisible views.promptPassword.showPassword(passwordVisible) - views.promptPasswordPasswordReveal.setImageResource(if (passwordVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye) + views.promptPasswordPasswordReveal.render(passwordVisible) } AlertDialog.Builder(activity) diff --git a/vector/src/main/java/im/vector/app/core/ui/views/RevealPasswordImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/RevealPasswordImageView.kt new file mode 100644 index 0000000000..d33f3ecbd0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/views/RevealPasswordImageView.kt @@ -0,0 +1,43 @@ +/* + * 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.core.ui.views + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView +import im.vector.app.R + +class RevealPasswordImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : AppCompatImageView(context, attrs, defStyleAttr) { + + init { + render(false) + } + + fun render(isPasswordShown: Boolean) { + if (isPasswordShown) { + setImageResource(R.drawable.ic_eye_closed) + contentDescription = context.getString(R.string.a11y_hide_password) + } else { + setImageResource(R.drawable.ic_eye) + contentDescription = context.getString(R.string.a11y_show_password) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt b/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt index 917f60dacb..4bdd54ae45 100644 --- a/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt +++ b/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt @@ -87,14 +87,7 @@ class PromptFragment : VectorBaseFragment() { } views.passwordField.showPassword(it.passwordVisible) - - if (it.passwordVisible) { - views.passwordReveal.setImageResource(R.drawable.ic_eye_closed) - views.passwordReveal.contentDescription = getString(R.string.a11y_hide_password) - } else { - views.passwordReveal.setImageResource(R.drawable.ic_eye) - views.passwordReveal.contentDescription = getString(R.string.a11y_show_password) - } + views.passwordReveal.render(it.passwordVisible) if (it.lastErrorCode != null) { when (it.flowType) { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt index 6c139aaf45..d15446dd62 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt @@ -60,7 +60,7 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor() : VectorBase viewModel.showPasswordMode.observe(viewLifecycleOwner, Observer { val shouldBeVisible = it ?: false views.keysBackupPassphraseEnterEdittext.showPassword(shouldBeVisible) - views.keysBackupViewShowPassword.setImageResource(if (shouldBeVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye) + views.keysBackupViewShowPassword.render(shouldBeVisible) }) views.keysBackupPassphraseEnterEdittext.setOnEditorActionListener { _, actionId, _ -> diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt index e739df8ce4..7f958d0e0a 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt @@ -119,7 +119,7 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment val shouldBeVisible = state.passphraseVisible views.ssssPassphraseEnterEdittext.showPassword(shouldBeVisible) - views.ssssViewShowPassword.setImageResource(if (shouldBeVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye) + views.ssssViewShowPassword.render(shouldBeVisible) } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt index 9862a97db2..2d26436556 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt @@ -109,7 +109,7 @@ class BootstrapConfirmPassphraseFragment @Inject constructor() if (state.step is BootstrapStep.ConfirmPassphrase) { val isPasswordVisible = state.step.isPasswordVisible views.ssssPassphraseEnterEdittext.showPassword(isPasswordVisible, updateCursor = false) - views.ssssViewShowPassword.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye) + views.ssssViewShowPassword.render(isPasswordVisible) } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt index 9d3f26cba8..7a5d6e5fd7 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt @@ -103,7 +103,7 @@ class BootstrapEnterPassphraseFragment @Inject constructor() if (state.step is BootstrapStep.SetupPassphrase) { val isPasswordVisible = state.step.isPasswordVisible views.ssssPassphraseEnterEdittext.showPassword(isPasswordVisible, updateCursor = false) - views.ssssViewShowPassword.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye) + views.ssssViewShowPassword.render(isPasswordVisible) state.passphraseStrength.invoke()?.let { strength -> val score = strength.score diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt index 99507fd1c7..ca0942f59a 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt @@ -133,7 +133,7 @@ class BootstrapMigrateBackupFragment @Inject constructor( if (state.step is BootstrapStep.GetBackupSecretPassForMigration) { val isPasswordVisible = state.step.isPasswordVisible views.bootstrapMigrateEditText.showPassword(isPasswordVisible, updateCursor = false) - views.bootstrapMigrateShowPassword.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye) + views.bootstrapMigrateShowPassword.render(isPasswordVisible) } views.bootstrapDescriptionText.text = getString(R.string.bootstrap_migration_enter_backup_password) diff --git a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt index 3b22e0f206..8f18fd8296 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt @@ -255,14 +255,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment() { @@ -76,20 +76,13 @@ abstract class LoginPasswordFormItem : VectorEpoxyModel(R.id.itemLoginPasswordFormPasswordField) val passwordFieldTil by bind(R.id.itemLoginPasswordFormPasswordFieldTil) - val passwordReveal by bind(R.id.itemLoginPasswordFormPasswordReveal) + val passwordReveal by bind(R.id.itemLoginPasswordFormPasswordReveal) val forgetPassword by bind