Dev tools initial commit

This commit is contained in:
Valere 2021-02-10 10:05:41 +01:00
parent 08b65b57e4
commit e5da026f1f
30 changed files with 1411 additions and 8 deletions

View File

@ -23,7 +23,7 @@ Test:
-
Other changes:
-
- New Dev Tools panel for developers
Changes in Element 1.0.17 (2020-02-09)
===================================================

View File

@ -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<PublicKeys> = emptyList()
@Json(name = "public_keys") val publicKeys: List<PublicKeys>? = emptyList()
)
@JsonClass(generateAdapter = true)

View File

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

View File

@ -80,7 +80,11 @@ internal class StateEventDataSource @Inject constructor(@SessionDatabase private
): RealmQuery<CurrentStateEventEntity> {
return realm.where<CurrentStateEventEntity>()
.equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
.`in`(CurrentStateEventEntityFields.TYPE, eventTypes.toTypedArray())
.apply {
if (eventTypes.isNotEmpty()) {
`in`(CurrentStateEventEntityFields.TYPE, eventTypes.toTypedArray())
}
}
.process(CurrentStateEventEntityFields.STATE_KEY, stateKey)
}
}

View File

@ -262,6 +262,7 @@
<!-- </intent-filter>-->
</activity>
<activity android:name=".features.devtools.RoomDevToolActivity"/>
<!-- Services -->
<service

View File

@ -45,6 +45,10 @@ import im.vector.app.features.crypto.verification.emoji.VerificationEmojiCodeFra
import im.vector.app.features.crypto.verification.qrconfirmation.VerificationQRWaitingFragment
import im.vector.app.features.crypto.verification.qrconfirmation.VerificationQrScannedByOtherFragment
import im.vector.app.features.crypto.verification.request.VerificationRequestFragment
import im.vector.app.features.devtools.RoomDevToolEditFragment
import im.vector.app.features.devtools.RoomDevToolFragment
import im.vector.app.features.devtools.RoomDevToolSendFormFragment
import im.vector.app.features.devtools.RoomDevToolStateEventListFragment
import im.vector.app.features.discovery.DiscoverySettingsFragment
import im.vector.app.features.discovery.change.SetIdentityServerFragment
import im.vector.app.features.grouplist.GroupListFragment
@ -594,4 +598,24 @@ interface FragmentModule {
@IntoMap
@FragmentKey(ShowUserCodeFragment::class)
fun bindShowUserCodeFragment(fragment: ShowUserCodeFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomDevToolFragment::class)
fun bindRoomDevToolFragment(fragment: RoomDevToolFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomDevToolStateEventListFragment::class)
fun bindRoomDevToolStateEventListFragment(fragment: RoomDevToolStateEventListFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomDevToolEditFragment::class)
fun bindRoomDevToolEditFragment(fragment: RoomDevToolEditFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomDevToolSendFormFragment::class)
fun bindRoomDevToolSendFormFragment(fragment: RoomDevToolSendFormFragment): Fragment
}

View File

@ -35,6 +35,7 @@ import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
import im.vector.app.features.crypto.verification.VerificationBottomSheet
import im.vector.app.features.debug.DebugMenuActivity
import im.vector.app.features.devtools.RoomDevToolActivity
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.HomeModule
import im.vector.app.features.home.room.detail.RoomDetailActivity
@ -147,6 +148,7 @@ interface ScreenComponent {
fun inject(activity: SearchActivity)
fun inject(activity: UserCodeActivity)
fun inject(activity: ReAuthActivity)
fun inject(activity: RoomDevToolActivity)
/* ==========================================================================================
* BottomSheets

View File

@ -48,7 +48,7 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
}
@EpoxyAttribute
var title: String? = null
var title: CharSequence? = null
@EpoxyAttribute
var description: CharSequence? = null

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<RoomDevToolViewState, RoomDevToolAction, DevToolsViewEvents>(initialState) {
@AssistedFactory
interface Factory {
fun create(initialState: RoomDevToolViewState): RoomDevToolViewModel
}
companion object : MvRxViewModelFactory<RoomDevToolViewModel, RoomDevToolViewState> {
@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<JsonDict>(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<JsonDict>(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<MessageContent>(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
)
}
}
}
}
}

View File

@ -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<List<Event>> = Uninitialized,
val currentStateType: String? = null,
val selectedEvent: Event? = null,
val selectedEventJson: String? = null,
val editedContent: String? = null,
val modalLoading: Async<Unit> = 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?
)
}

View File

@ -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<RoomDevToolViewState>() {
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<String, Any>()).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
}
}
}
}

View File

@ -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<FormMultiLineEditTextItem.Holder>() {
@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<TextInputLayout>(R.id.formMultiLineTextInputLayout)
val textInputEditText by bind<TextInputEditText>(R.id.formMultiLineEditText)
val bottomSeparator by bind<View>(R.id.formTextInputDivider)
}
}

View File

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

View File

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

View File

@ -63,7 +63,8 @@ class RoomMemberListController @Inject constructor(
?.filter { event ->
event.content.toModel<RoomThirdPartyInviteContent>()
?.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()

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="top|start"
android:textSize="12sp"
android:fontFamily="monospace"
android:scrollHorizontally="true"
android:inputType="textMultiLine" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:minHeight="@dimen/item_form_min_height">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/formMultiLineTextInputLayout"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
app:errorEnabled="true"
app:layout_constraintBottom_toTopOf="@+id/formTextInputDivider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<!-- android:imeOptions="actionDone" to fix a crash -->
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/formMultiLineEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:imeOptions="actionDone"
android:gravity="top|start"
android:minLines="4"
tools:hint="@string/create_room_name_hint" />
</com.google.android.material.textfield.TextInputLayout>
<View
android:id="@+id/formTextInputDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?riotx_header_panel_border_mobile"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

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

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menuItemEdit"
android:visible="false"
tools:visible="true"
app:showAsAction="ifRoom"
android:icon="@drawable/ic_edit"
android:title="@string/edit" />
<item
android:id="@+id/menuItemSend"
android:visible="false"
tools:visible="true"
app:showAsAction="ifRoom"
android:icon="@drawable/ic_send"
android:title="@string/send" />
</menu>

View File

@ -68,4 +68,12 @@
app:showAsAction="never"
tools:visible="true" />
<item
android:id="@+id/dev_tools"
android:icon="@drawable/ic_settings_root_general"
android:title="@string/dev_tools_menu_name"
android:visible="false"
app:showAsAction="never"
tools:visible="true" />
</menu>

View File

@ -2792,4 +2792,5 @@
<string name="re_authentication_activity_title">Re-Authentication Needed</string>
<string name="re_authentication_default_confirm_text">Element requires you to enter your credentials to perform this action.</string>
<string name="authentication_error">Failed to authenticate</string>
<string name="dev_tools_menu_name">Dev Tools</string>
</resources>