Merge pull request #2805 from vector-im/feature/bca/devtools

Dev tools initial commit
This commit is contained in:
Benoit Marty 2021-02-16 09:46:49 +01:00 committed by GitHub
commit b3a408a34c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1475 additions and 11 deletions

View file

@ -26,6 +26,7 @@ Test:
- -
Other changes: Other changes:
- New Dev Tools panel for developers
- Fix typos in CHANGES.md (#2811) - Fix typos in CHANGES.md (#2811)
Changes in Element 1.0.17 (2021-02-09) Changes in Element 1.0.17 (2021-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 * 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. * 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 * 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'. * 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 * 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. * 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. * 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) @JsonClass(generateAdapter = true)

View file

@ -65,13 +65,30 @@ interface StateService {
*/ */
suspend fun deleteAvatar() suspend fun deleteAvatar()
/**
* Send a state event to the room
*/
suspend fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict) suspend fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict)
/**
* Get a state event of the room
*/
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
/**
* Get a live state event of the room
*/
fun getStateEventLive(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<Optional<Event>> fun getStateEventLive(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<Optional<Event>>
/**
* Get state events of the room
* @param eventTypes Set of eventType. If empty, all state events will be returned
*/
fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): List<Event> fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): List<Event>
/**
* Get live state events of the room
* @param eventTypes Set of eventType to observe. If empty, all state events will be observed
*/
fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<List<Event>> fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<List<Event>>
} }

View file

@ -196,6 +196,7 @@ internal class EventSenderProcessor @Inject constructor(
else -> { else -> {
Timber.v("## SendThread retryLoop Un-Retryable error, try next task") Timber.v("## SendThread retryLoop Un-Retryable error, try next task")
// this task is in error, check next one? // this task is in error, check next one?
task.onTaskFailed()
break@retryLoop break@retryLoop
} }
} }

View file

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

View file

@ -267,6 +267,7 @@
<!-- </intent-filter>--> <!-- </intent-filter>-->
</activity> </activity>
<activity android:name=".features.devtools.RoomDevToolActivity"/>
<!-- Services --> <!-- Services -->
<service <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.VerificationQRWaitingFragment
import im.vector.app.features.crypto.verification.qrconfirmation.VerificationQrScannedByOtherFragment import im.vector.app.features.crypto.verification.qrconfirmation.VerificationQrScannedByOtherFragment
import im.vector.app.features.crypto.verification.request.VerificationRequestFragment 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.DiscoverySettingsFragment
import im.vector.app.features.discovery.change.SetIdentityServerFragment import im.vector.app.features.discovery.change.SetIdentityServerFragment
import im.vector.app.features.grouplist.GroupListFragment import im.vector.app.features.grouplist.GroupListFragment
@ -594,4 +598,24 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(ShowUserCodeFragment::class) @FragmentKey(ShowUserCodeFragment::class)
fun bindShowUserCodeFragment(fragment: ShowUserCodeFragment): Fragment 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

@ -36,6 +36,7 @@ import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
import im.vector.app.features.crypto.recover.BootstrapBottomSheet import im.vector.app.features.crypto.recover.BootstrapBottomSheet
import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.crypto.verification.VerificationBottomSheet
import im.vector.app.features.debug.DebugMenuActivity 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.HomeActivity
import im.vector.app.features.home.HomeModule import im.vector.app.features.home.HomeModule
import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailActivity
@ -149,6 +150,7 @@ interface ScreenComponent {
fun inject(activity: UserCodeActivity) fun inject(activity: UserCodeActivity)
fun inject(activity: CallTransferActivity) fun inject(activity: CallTransferActivity)
fun inject(activity: ReAuthActivity) fun inject(activity: ReAuthActivity)
fun inject(activity: RoomDevToolActivity)
/* ========================================================================================== /* ==========================================================================================
* BottomSheets * BottomSheets

View file

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

View file

@ -0,0 +1,21 @@
/*
* 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
interface DevToolsInteractionListener {
fun processAction(action: RoomDevToolAction)
}

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,256 @@
/*
* 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.exhaustive
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
private 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()
Unit
}
is DevToolsViewEvents.ShowSnackMessage -> showSnackbar(it.message)
}.exhaustive
}
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(
jsonString = it.selectedEventJson ?: "",
initialOpenDepth = -1,
wrap = true,
styleProvider = 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(context: Context, roomId: String): 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 -> {
getString(R.string.dev_tools_state_event)
}
RoomDevToolViewState.Mode.StateEventDetail -> {
state.selectedEvent?.type
}
RoomDevToolViewState.Mode.EditEventContent -> {
getString(R.string.dev_tools_edit_content)
}
RoomDevToolViewState.Mode.StateEventListByType -> {
state.currentStateType ?: ""
}
is RoomDevToolViewState.Mode.SendEventForm -> {
getString(
if (state.displayMode.isState) R.string.dev_tools_send_custom_state_event
else R.string.dev_tools_send_custom_event
)
}
}
supportActionBar?.let {
it.title = title
} ?: run {
setTitle(title)
}
invalidateOptionsMenu()
}
}

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 com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentDevtoolsEditorBinding
import javax.inject.Inject
class RoomDevToolEditFragment @Inject constructor()
: VectorBaseFragment<FragmentDevtoolsEditorBinding>() {
private 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 onResume() {
super.onResume()
views.editText.requestFocus()
}
override fun onStop() {
super.onStop()
views.editText.hideKeyboard()
}
}

View file

@ -0,0 +1,66 @@
/*
* 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 im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentGenericRecyclerBinding
import javax.inject.Inject
class RoomDevToolFragment @Inject constructor(
private val epoxyController: RoomDevToolRootController
) : VectorBaseFragment<FragmentGenericRecyclerBinding>(),
DevToolsInteractionListener {
private 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 processAction(action: RoomDevToolAction) {
sharedViewModel.handle(action)
}
}

View file

@ -0,0 +1,59 @@
/*
* 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.EpoxyController
import im.vector.app.R
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
) : EpoxyController() {
init {
requestModelBuild()
}
var interactionListener: DevToolsInteractionListener? = null
override fun buildModels() {
genericButtonItem {
id("explore")
text(stringProvider.getString(R.string.dev_tools_explore_room_state))
buttonClickAction(View.OnClickListener {
interactionListener?.processAction(RoomDevToolAction.ExploreRoomState)
})
}
genericButtonItem {
id("send")
text(stringProvider.getString(R.string.dev_tools_send_custom_event))
buttonClickAction(View.OnClickListener {
interactionListener?.processAction(RoomDevToolAction.SendCustomEvent(false))
})
}
genericButtonItem {
id("send_state")
text(stringProvider.getString(R.string.dev_tools_send_state_event))
buttonClickAction(View.OnClickListener {
interactionListener?.processAction(RoomDevToolAction.SendCustomEvent(true))
})
}
}
}

View file

@ -0,0 +1,75 @@
/*
* 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.resources.StringProvider
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(
private val stringProvider: StringProvider
) : TypedEpoxyController<RoomDevToolViewState>() {
var interactionListener: DevToolsInteractionListener? = null
override fun buildModels(data: RoomDevToolViewState?) {
val sendEventForm = (data?.displayMode as? RoomDevToolViewState.Mode.SendEventForm) ?: return
genericFooterItem {
id("topSpace")
text("")
}
formEditTextItem {
id("event_type")
enabled(true)
value(data.sendEventDraft?.type)
hint(stringProvider.getString(R.string.dev_tools_form_hint_type))
showBottomSeparator(false)
onTextChange { text ->
interactionListener?.processAction(RoomDevToolAction.CustomEventTypeChange(text))
}
}
if (sendEventForm.isState) {
formEditTextItem {
id("state_key")
enabled(true)
value(data.sendEventDraft?.stateKey)
hint(stringProvider.getString(R.string.dev_tools_form_hint_state_key))
showBottomSeparator(false)
onTextChange { text ->
interactionListener?.processAction(RoomDevToolAction.CustomEventStateKeyChange(text))
}
}
}
formMultiLineEditTextItem {
id("event_content")
enabled(true)
value(data.sendEventDraft?.content)
hint(stringProvider.getString(R.string.dev_tools_form_hint_event_content))
showBottomSeparator(false)
onTextChange { text ->
interactionListener?.processAction(RoomDevToolAction.CustomEventContentChange(text))
}
}
}
}

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.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.databinding.FragmentGenericRecyclerBinding
import javax.inject.Inject
class RoomDevToolSendFormFragment @Inject constructor(
private val epoxyController: RoomDevToolSendFormController
) : VectorBaseFragment<FragmentGenericRecyclerBinding>(), DevToolsInteractionListener {
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,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.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.databinding.FragmentGenericRecyclerBinding
import javax.inject.Inject
class RoomDevToolStateEventListFragment @Inject constructor(
private val epoxyController: RoomStateListController
) : VectorBaseFragment<FragmentGenericRecyclerBinding>(), DevToolsInteractionListener {
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,304 @@
/*
* 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.R
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
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.EventType
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 stringProvider: StringProvider,
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(EventType.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 { state ->
when (state.displayMode) {
RoomDevToolViewState.Mode.EditEventContent -> editEventContent(state)
is RoomDevToolViewState.Mode.SendEventForm -> sendEventContent(state, state.displayMode.isState)
else -> Unit
}
}
private fun editEventContent(state: RoomDevToolViewState) {
setState { copy(modalLoading = Loading()) }
viewModelScope.launch {
try {
val room = session.getRoom(initialState.roomId)
?: throw IllegalArgumentException(stringProvider.getString(R.string.room_error_not_found))
val adapter = MoshiProvider.providesMoshi()
.adapter<JsonDict>(Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java))
val json = adapter.fromJson(state.editedContent ?: "")
?: throw IllegalArgumentException(stringProvider.getString(R.string.dev_tools_error_no_content))
room.sendStateEvent(
state.selectedEvent?.type ?: "",
state.selectedEvent?.stateKey,
json
)
_viewEvents.post(DevToolsViewEvents.ShowSnackMessage(stringProvider.getString(R.string.dev_tools_success_state_event)))
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)) }
}
}
}
private fun sendEventContent(state: RoomDevToolViewState, isState: Boolean) {
setState { copy(modalLoading = Loading()) }
viewModelScope.launch {
try {
val room = session.getRoom(initialState.roomId)
?: throw IllegalArgumentException(stringProvider.getString(R.string.room_error_not_found))
val adapter = MoshiProvider.providesMoshi()
.adapter<JsonDict>(Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java))
val json = adapter.fromJson(state.sendEventDraft?.content ?: "")
?: throw IllegalArgumentException(stringProvider.getString(R.string.dev_tools_error_no_content))
val eventType = state.sendEventDraft?.type
?: throw IllegalArgumentException(stringProvider.getString(R.string.dev_tools_error_no_message_type))
if (isState) {
room.sendStateEvent(
eventType,
state.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(stringProvider.getString(R.string.dev_tools_error_malformed_event))
room.sendEvent(
eventType,
json
)
}
_viewEvents.post(DevToolsViewEvents.ShowSnackMessage(stringProvider.getString(R.string.dev_tools_success_event)))
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,109 @@
/*
* 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>() {
var interactionListener: DevToolsInteractionListener? = null
override fun buildModels(data: RoomDevToolViewState?) {
when (data?.displayMode) {
RoomDevToolViewState.Mode.StateEventList -> {
val stateEventsGroups = data.stateEvents.invoke().orEmpty().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(stringProvider.getQuantityString(R.plurals.entries, entry.value.size, entry.value.size))
itemClickAction(GenericItem.Action("view").apply {
perform = Runnable {
interactionListener?.processAction(RoomDevToolAction.ShowStateEventType(entry.key))
}
})
}
}
}
}
RoomDevToolViewState.Mode.StateEventListByType -> {
val stateEvents = data.stateEvents.invoke().orEmpty().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 contentJson = JSONObject(stateEvent.content.orEmpty()).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(contentJson)
itemClickAction(GenericItem.Action("view").apply {
perform = Runnable {
interactionListener?.processAction(RoomDevToolAction.ShowStateEvent(stateEvent))
}
})
}
}
}
}
else -> {
// nop
}
}
}
}

View file

@ -0,0 +1,101 @@
/*
* 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.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

@ -789,6 +789,10 @@ class RoomDetailFragment @Inject constructor(
handleSearchAction() handleSearchAction()
true true
} }
R.id.dev_tools -> {
navigator.openDevTools(requireContext(), roomDetailArgs.roomId)
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }

View file

@ -625,10 +625,11 @@ class RoomDetailViewModel @AssistedInject constructor(
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.open_matrix_apps -> true R.id.open_matrix_apps -> true
R.id.voice_call, R.id.voice_call,
R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty() R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty()
R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty() R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty()
R.id.search -> true R.id.search -> true
else -> false R.id.dev_tools -> vectorPreferences.developerMode()
else -> false
} }
} }

View file

@ -45,6 +45,7 @@ import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.crypto.verification.VerificationBottomSheet
import im.vector.app.features.debug.DebugMenuActivity import im.vector.app.features.debug.DebugMenuActivity
import im.vector.app.features.devtools.RoomDevToolActivity
import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailActivity
import im.vector.app.features.home.room.detail.RoomDetailArgs import im.vector.app.features.home.room.detail.RoomDetailArgs
import im.vector.app.features.home.room.detail.search.SearchActivity import im.vector.app.features.home.room.detail.search.SearchActivity
@ -357,6 +358,10 @@ class DefaultNavigator @Inject constructor(
context.startActivity(intent) context.startActivity(intent)
} }
override fun openDevTools(context: Context, roomId: String) {
context.startActivity(RoomDevToolActivity.intent(context, roomId))
}
override fun openCallTransfer(context: Context, callId: String) { override fun openCallTransfer(context: Context, callId: String) {
val intent = CallTransferActivity.newIntent(context, callId) val intent = CallTransferActivity.newIntent(context, callId)
context.startActivity(intent) context.startActivity(intent)

View file

@ -118,5 +118,7 @@ interface Navigator {
fun openSearch(context: Context, roomId: String) fun openSearch(context: Context, roomId: String)
fun openDevTools(context: Context, roomId: String)
fun openCallTransfer(context: Context, callId: String) fun openCallTransfer(context: Context, callId: String)
} }

View file

@ -63,7 +63,7 @@ class RoomMemberListController @Inject constructor(
?.filter { event -> ?.filter { event ->
event.content.toModel<RoomThirdPartyInviteContent>() event.content.toModel<RoomThirdPartyInviteContent>()
?.takeIf { ?.takeIf {
data.filter.isEmpty() || it.displayName.contains(data.filter, ignoreCase = true) data.filter.isEmpty() || it.displayName?.contains(data.filter, ignoreCase = true) == true
} != null } != null
} }
.orEmpty() .orEmpty()

View file

@ -0,0 +1,20 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/editText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:gravity="top|start"
android:inputType="textMultiLine"
android:scrollHorizontally="true"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</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:gravity="top|start"
android:imeOptions="actionDone"
android:inputType="textMultiLine"
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:id="@+id/item_generic_root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:minHeight="50dp"> android:minHeight="50dp">
<ImageView <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" app:showAsAction="never"
tools:visible="true" /> 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> </menu>

View file

@ -2750,6 +2750,10 @@
<item quantity="one">Wrong code, %d remaining attempt</item> <item quantity="one">Wrong code, %d remaining attempt</item>
<item quantity="other">Wrong code, %d remaining attempts</item> <item quantity="other">Wrong code, %d remaining attempts</item>
</plurals> </plurals>
<plurals name="entries">
<item quantity="one">%d entry"</item>
<item quantity="other">%d entries"</item>
</plurals>
<string name="wrong_pin_message_last_remaining_attempt">Warning! Last remaining attempt before logout!</string> <string name="wrong_pin_message_last_remaining_attempt">Warning! Last remaining attempt before logout!</string>
<string name="too_many_pin_failures">Too many errors, you\'ve been logged out</string> <string name="too_many_pin_failures">Too many errors, you\'ve been logged out</string>
<string name="create_pin_title">Choose a PIN for security</string> <string name="create_pin_title">Choose a PIN for security</string>
@ -2827,4 +2831,20 @@
<string name="re_authentication_activity_title">Re-Authentication Needed</string> <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="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="authentication_error">Failed to authenticate</string>
<string name="dev_tools_menu_name">Dev Tools</string>
<string name="dev_tools_explore_room_state">Explore Room State</string>
<string name="dev_tools_send_custom_event">Send Custom Event</string>
<string name="dev_tools_send_state_event">Send State Event</string>
<string name="dev_tools_state_event">State Events</string>
<string name="dev_tools_edit_content">Edit Content</string>
<string name="dev_tools_send_custom_state_event">Send Custom State Event</string>
<string name="dev_tools_form_hint_type">Type</string>
<string name="dev_tools_form_hint_state_key">State Key</string>
<string name="dev_tools_form_hint_event_content">Event Content</string>
<string name="dev_tools_error_no_content">No content</string>
<string name="dev_tools_error_no_message_type">Missing message type</string>
<string name="dev_tools_error_malformed_event">Malformed event</string>
<string name="dev_tools_success_event">Event sent!</string>
<string name="dev_tools_success_state_event">State event sent!</string>
</resources> </resources>