mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Merge pull request #2805 from vector-im/feature/bca/devtools
Dev tools initial commit
This commit is contained in:
commit
b3a408a34c
@ -26,6 +26,7 @@ Test:
|
||||
-
|
||||
|
||||
Other changes:
|
||||
- New Dev Tools panel for developers
|
||||
- Fix typos in CHANGES.md (#2811)
|
||||
|
||||
Changes in Element 1.0.17 (2021-02-09)
|
||||
|
@ -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)
|
||||
|
@ -65,13 +65,30 @@ interface StateService {
|
||||
*/
|
||||
suspend fun deleteAvatar()
|
||||
|
||||
/**
|
||||
* Send a state event to the room
|
||||
*/
|
||||
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?
|
||||
|
||||
/**
|
||||
* Get a live state event of the room
|
||||
*/
|
||||
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>
|
||||
|
||||
/**
|
||||
* 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>>
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -267,6 +267,7 @@
|
||||
<!-- </intent-filter>-->
|
||||
</activity>
|
||||
|
||||
<activity android:name=".features.devtools.RoomDevToolActivity"/>
|
||||
<!-- Services -->
|
||||
|
||||
<service
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.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
|
||||
@ -149,6 +150,7 @@ interface ScreenComponent {
|
||||
fun inject(activity: UserCodeActivity)
|
||||
fun inject(activity: CallTransferActivity)
|
||||
fun inject(activity: ReAuthActivity)
|
||||
fun inject(activity: RoomDevToolActivity)
|
||||
|
||||
/* ==========================================================================================
|
||||
* BottomSheets
|
||||
|
@ -48,7 +48,7 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
|
||||
}
|
||||
|
||||
@EpoxyAttribute
|
||||
var title: String? = null
|
||||
var title: CharSequence? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var description: CharSequence? = null
|
||||
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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?
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -789,6 +789,10 @@ class RoomDetailFragment @Inject constructor(
|
||||
handleSearchAction()
|
||||
true
|
||||
}
|
||||
R.id.dev_tools -> {
|
||||
navigator.openDevTools(requireContext(), roomDetailArgs.roomId)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
@ -625,10 +625,11 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
|
||||
R.id.open_matrix_apps -> true
|
||||
R.id.voice_call,
|
||||
R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty()
|
||||
R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty()
|
||||
R.id.search -> true
|
||||
else -> false
|
||||
R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty()
|
||||
R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty()
|
||||
R.id.search -> true
|
||||
R.id.dev_tools -> vectorPreferences.developerMode()
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.VerificationBottomSheet
|
||||
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.RoomDetailArgs
|
||||
import im.vector.app.features.home.room.detail.search.SearchActivity
|
||||
@ -357,6 +358,10 @@ class DefaultNavigator @Inject constructor(
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun openDevTools(context: Context, roomId: String) {
|
||||
context.startActivity(RoomDevToolActivity.intent(context, roomId))
|
||||
}
|
||||
|
||||
override fun openCallTransfer(context: Context, callId: String) {
|
||||
val intent = CallTransferActivity.newIntent(context, callId)
|
||||
context.startActivity(intent)
|
||||
|
@ -118,5 +118,7 @@ interface Navigator {
|
||||
|
||||
fun openSearch(context: Context, roomId: String)
|
||||
|
||||
fun openDevTools(context: Context, roomId: String)
|
||||
|
||||
fun openCallTransfer(context: Context, callId: String)
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ class RoomMemberListController @Inject constructor(
|
||||
?.filter { event ->
|
||||
event.content.toModel<RoomThirdPartyInviteContent>()
|
||||
?.takeIf {
|
||||
data.filter.isEmpty() || it.displayName.contains(data.filter, ignoreCase = true)
|
||||
data.filter.isEmpty() || it.displayName?.contains(data.filter, ignoreCase = true) == true
|
||||
} != null
|
||||
}
|
||||
.orEmpty()
|
||||
|
20
vector/src/main/res/layout/fragment_devtools_editor.xml
Normal file
20
vector/src/main/res/layout/fragment_devtools_editor.xml
Normal 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>
|
@ -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>
|
@ -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
|
||||
|
22
vector/src/main/res/menu/menu_devtools.xml
Normal file
22
vector/src/main/res/menu/menu_devtools.xml
Normal 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>
|
@ -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>
|
@ -2750,6 +2750,10 @@
|
||||
<item quantity="one">Wrong code, %d remaining attempt</item>
|
||||
<item quantity="other">Wrong code, %d remaining attempts</item>
|
||||
</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="too_many_pin_failures">Too many errors, you\'ve been logged out</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_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>
|
||||
<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>
|
||||
|
Loading…
Reference in New Issue
Block a user