Merge pull request #976 from vector-im/feature/viewEvents

Use View events
This commit is contained in:
Benoit Marty 2020-02-10 16:46:21 +01:00 committed by GitHub
commit 6750237764
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 583 additions and 526 deletions

View File

@ -37,6 +37,9 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.utils.DimensionConverter
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import timber.log.Timber
/**
@ -87,10 +90,18 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
return view
}
@CallSuper
override fun onDestroyView() {
super.onDestroyView()
unBinder?.unbind()
unBinder = null
uiDisposables.clear()
}
@CallSuper
override fun onDestroy() {
uiDisposables.dispose()
super.onDestroy()
}
override fun onAttach(context: Context) {
@ -146,4 +157,29 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
protected fun setArguments(args: Parcelable? = null) {
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
}
/* ==========================================================================================
* Disposable
* ========================================================================================== */
private val uiDisposables = CompositeDisposable()
protected fun Disposable.disposeOnDestroyView(): Disposable {
uiDisposables.add(this)
return this
}
/* ==========================================================================================
* ViewEvents
* ========================================================================================== */
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
observer(it)
}
.disposeOnDestroyView()
}
}

View File

@ -31,7 +31,11 @@ import com.google.android.material.chip.ChipGroup
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R
import im.vector.riotx.core.extensions.*
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.setupAsSearch
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.DimensionConverter
import kotlinx.android.synthetic.main.fragment_create_direct_room.*
@ -57,8 +61,10 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
setupFilterView()
setupAddByMatrixIdView()
setupCloseView()
viewModel.selectUserEvent.observeEvent(this) {
updateChipsView(it)
viewModel.observeViewEvents {
when (it) {
is CreateDirectRoomViewEvents.SelectUserAction -> updateChipsView(it)
}.exhaustive
}
viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) {
renderSelectedUsers(it)
@ -132,7 +138,7 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
knownUsersController.setData(it)
}
private fun updateChipsView(data: SelectUserAction) {
private fun updateChipsView(data: CreateDirectRoomViewEvents.SelectUserAction) {
if (data.isAdded) {
addChipToGroup(data.user, chipGroup)
} else {

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.createdirect
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for create direct room screen
*/
sealed class CreateDirectRoomViewEvents : VectorViewEvents {
data class SelectUserAction(
val user: User,
val isAdded: Boolean,
val index: Int
) : CreateDirectRoomViewEvents()
}

View File

@ -18,8 +18,6 @@
package im.vector.riotx.features.createdirect
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
@ -32,10 +30,7 @@ import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
@ -43,16 +38,10 @@ import java.util.concurrent.TimeUnit
private typealias KnowUsersFilter = String
private typealias DirectoryUsersSearch = String
data class SelectUserAction(
val user: User,
val isAdded: Boolean,
val index: Int
)
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState,
private val session: Session)
: VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, EmptyViewEvents>(initialState) {
: VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -62,10 +51,6 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
private val _selectUserEvent = MutableLiveData<LiveEvent<SelectUserAction>>()
val selectUserEvent: LiveData<LiveEvent<SelectUserAction>>
get() = _selectUserEvent
companion object : MvRxViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> {
@JvmStatic
@ -109,7 +94,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
val index = state.selectedUsers.indexOfFirst { it.userId == action.user.userId }
val selectedUsers = state.selectedUsers.minus(action.user)
setState { copy(selectedUsers = selectedUsers) }
_selectUserEvent.postLiveEvent(SelectUserAction(action.user, false, index))
_viewEvents.post(CreateDirectRoomViewEvents.SelectUserAction(action.user, false, index))
}
private fun handleSelectUser(action: CreateDirectRoomAction.SelectUser) = withState { state ->
@ -129,7 +114,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
isAddOperation = false
}
setState { copy(selectedUsers = selectedUsers) }
_selectUserEvent.postLiveEvent(SelectUserAction(action.user, isAddOperation, changeIndex))
_viewEvents.post(CreateDirectRoomViewEvents.SelectUserAction(action.user, isAddOperation, changeIndex))
}
private fun observeDirectoryUsers() {

View File

@ -23,18 +23,17 @@ import android.widget.TextView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import butterknife.BindView
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
@ -56,7 +55,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
val verificationId: String? = null,
val roomId: String? = null,
// Special mode where UX should show loading wheel until other user sends a request/tx
val waitForIncomingRequest : Boolean = false
val waitForIncomingRequest: Boolean = false
) : Parcelable
@Inject
@ -84,17 +83,11 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.requestLiveData.observe(viewLifecycleOwner, Observer {
it.peekContent().let { va ->
when (va) {
is Success -> {
if (va.invoke() is VerificationAction.GotItConclusion) {
dismiss()
}
}
}
}
})
viewModel.observeViewEvents {
when (it) {
is VerificationBottomSheetViewEvents.Dismiss -> dismiss()
}.exhaustive
}
}
override fun invalidate() = withState(viewModel) { state ->
@ -250,7 +243,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
}
val WAITING_SELF_VERIF_TAG : String = "WAITING_SELF_VERIF_TAG"
const val WAITING_SELF_VERIF_TAG: String = "WAITING_SELF_VERIF_TAG"
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.crypto.verification
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for the verification bottom sheet
*/
sealed class VerificationBottomSheetViewEvents : VectorViewEvents {
object Dismiss : VerificationBottomSheetViewEvents()
}

View File

@ -15,8 +15,6 @@
*/
package im.vector.riotx.features.crypto.verification
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
@ -43,9 +41,7 @@ import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
data class VerificationBottomSheetViewState(
val otherUserMxItem: MatrixItem? = null,
@ -63,14 +59,9 @@ data class VerificationBottomSheetViewState(
class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState,
@Assisted args: VerificationBottomSheet.VerificationArgs,
private val session: Session)
: VectorViewModel<VerificationBottomSheetViewState, VerificationAction, EmptyViewEvents>(initialState),
: VectorViewModel<VerificationBottomSheetViewState, VerificationAction, VerificationBottomSheetViewEvents>(initialState),
VerificationService.VerificationListener {
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<VerificationAction>>>()
val requestLiveData: LiveData<LiveEvent<Async<VerificationAction>>>
get() = _requestLiveData
init {
session.getVerificationService().addListener(this)
@ -255,7 +246,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
?.shortCodeDoesNotMatch()
}
is VerificationAction.GotItConclusion -> {
_requestLiveData.postValue(LiveEvent(Success(action)))
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
}
}.exhaustive
}

View File

@ -26,7 +26,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.HomeActivitySharedAction
@ -51,8 +51,10 @@ class GroupListFragment @Inject constructor(
stateView.contentView = groupListView
groupListView.configureWith(groupController)
viewModel.subscribe { renderState(it) }
viewModel.openGroupLiveData.observeEvent(this) {
sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup)
viewModel.observeViewEvents {
when (it) {
is GroupListViewEvents.OpenGroupSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup)
}.exhaustive
}
}

View File

@ -1,11 +1,11 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* 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,
@ -14,12 +14,13 @@
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail
package im.vector.riotx.features.grouplist
import java.io.File
import im.vector.riotx.core.platform.VectorViewEvents
data class DownloadFileState(
val mimeType: String,
val file: File?,
val throwable: Throwable?
)
/**
* Transient events for group list screen
*/
sealed class GroupListViewEvents : VectorViewEvents {
object OpenGroupSummary : GroupListViewEvents()
}

View File

@ -17,8 +17,6 @@
package im.vector.riotx.features.grouplist
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
@ -32,11 +30,8 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.rx.rx
import im.vector.riotx.R
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
@ -46,7 +41,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
private val selectedGroupStore: SelectedGroupDataSource,
private val session: Session,
private val stringProvider: StringProvider
) : VectorViewModel<GroupListViewState, GroupListAction, EmptyViewEvents>(initialState) {
) : VectorViewModel<GroupListViewState, GroupListAction, GroupListViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -62,9 +57,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
}
}
private val _openGroupLiveData = MutableLiveData<LiveEvent<GroupSummary>>()
val openGroupLiveData: LiveData<LiveEvent<GroupSummary>>
get() = _openGroupLiveData
private var currentGroupId = ""
init {
observeGroupSummaries()
@ -74,10 +67,10 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
private fun observeSelectionState() {
selectSubscribe(GroupListViewState::selectedGroup) { groupSummary ->
if (groupSummary != null) {
val selectedGroup = _openGroupLiveData.value?.peekContent()
// We only want to open group if the updated selectedGroup is a different one.
if (selectedGroup?.groupId != groupSummary.groupId) {
_openGroupLiveData.postLiveEvent(groupSummary)
if (currentGroupId != groupSummary.groupId) {
currentGroupId = groupSummary.groupId
_viewEvents.post(GroupListViewEvents.OpenGroupSummary)
}
val optionGroup = Option.just(groupSummary)
selectedGroupStore.post(optionGroup)

View File

@ -89,7 +89,6 @@ import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.files.addEntryToDownloadManager
@ -253,12 +252,7 @@ class RoomDetailFragment @Inject constructor(
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
}
roomDetailViewModel.subscribe { renderState(it) }
roomDetailViewModel.sendMessageResultLiveData.observeEvent(viewLifecycleOwner) { renderSendMessageResult(it) }
roomDetailViewModel.nonBlockingPopAlert.observeEvent(this) { pair ->
val message = getString(pair.first, *pair.second.toTypedArray())
showSnackWithMessage(message, Snackbar.LENGTH_LONG)
}
sharedActionViewModel
.observe()
.subscribe {
@ -266,34 +260,10 @@ class RoomDetailFragment @Inject constructor(
}
.disposeOnDestroyView()
roomDetailViewModel.navigateToEvent.observeEvent(this) {
val scrollPosition = timelineEventController.searchPositionOfEvent(it)
if (scrollPosition == null) {
scrollOnHighlightedEventCallback.scheduleScrollTo(it)
} else {
recyclerView.stopScroll()
layoutManager.scrollToPosition(scrollPosition)
}
}
roomDetailViewModel.fileTooBigEvent.observeEvent(this) {
displayFileTooBigWarning(it)
}
roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
renderTombstoneEventHandling(it)
}
roomDetailViewModel.downloadedFileEvent.observeEvent(this) { downloadFileState ->
val activity = requireActivity()
if (downloadFileState.throwable != null) {
activity.toast(errorFormatter.toHumanReadable(downloadFileState.throwable))
} else if (downloadFileState.file != null) {
activity.toast(getString(R.string.downloaded_file, downloadFileState.file.path))
addEntryToDownloadManager(activity, downloadFileState.file, downloadFileState.mimeType)
}
}
roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode ->
when (mode) {
is SendMode.REGULAR -> renderRegularMode(mode.text)
@ -307,14 +277,17 @@ class RoomDetailFragment @Inject constructor(
syncStateView.render(syncState)
}
roomDetailViewModel.requestLiveData.observeEvent(this) {
displayRoomDetailActionResult(it)
}
roomDetailViewModel.observeViewEvents {
when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG)
is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
is RoomDetailViewEvents.FileTooBigError -> displayFileTooBigError(it)
is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it)
is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it)
}.exhaustive
}
}
@ -369,18 +342,38 @@ class RoomDetailFragment @Inject constructor(
jumpToReadMarkerView.callback = this
}
private fun displayFileTooBigWarning(error: FileTooBigError) {
private fun navigateToEvent(action: RoomDetailViewEvents.NavigateToEvent) {
val scrollPosition = timelineEventController.searchPositionOfEvent(action.eventId)
if (scrollPosition == null) {
scrollOnHighlightedEventCallback.scheduleScrollTo(action.eventId)
} else {
recyclerView.stopScroll()
layoutManager.scrollToPosition(scrollPosition)
}
}
private fun displayFileTooBigError(action: RoomDetailViewEvents.FileTooBigError) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(getString(R.string.error_file_too_big,
error.filename,
TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes),
TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes)
action.filename,
TextUtils.formatFileSize(requireContext(), action.fileSizeInBytes),
TextUtils.formatFileSize(requireContext(), action.homeServerLimitInBytes)
))
.setPositiveButton(R.string.ok, null)
.show()
}
private fun handleDownloadFileState(action: RoomDetailViewEvents.DownloadFileState) {
val activity = requireActivity()
if (action.throwable != null) {
activity.toast(errorFormatter.toHumanReadable(action.throwable))
} else if (action.file != null) {
activity.toast(getString(R.string.downloaded_file, action.file.path))
addEntryToDownloadManager(activity, action.file, action.mimeType)
}
}
private fun setupNotificationView() {
notificationAreaView.delegate = object : NotificationAreaView.Delegate {
override fun onTombstoneEventClicked(tombstoneEvent: Event) {
@ -739,31 +732,31 @@ class RoomDetailFragment @Inject constructor(
}
}
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
private fun renderSendMessageResult(sendMessageResult: RoomDetailViewEvents.SendMessageResult) {
when (sendMessageResult) {
is SendMessageResult.MessageSent -> {
is RoomDetailViewEvents.MessageSent -> {
updateComposerText("")
}
is SendMessageResult.SlashCommandHandled -> {
is RoomDetailViewEvents.SlashCommandHandled -> {
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
updateComposerText("")
}
is SendMessageResult.SlashCommandError -> {
is RoomDetailViewEvents.SlashCommandError -> {
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
}
is SendMessageResult.SlashCommandUnknown -> {
is RoomDetailViewEvents.SlashCommandUnknown -> {
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
}
is SendMessageResult.SlashCommandResultOk -> {
is RoomDetailViewEvents.SlashCommandResultOk -> {
updateComposerText("")
}
is SendMessageResult.SlashCommandResultError -> {
is RoomDetailViewEvents.SlashCommandResultError -> {
displayCommandError(sendMessageResult.throwable.localizedMessage)
}
is SendMessageResult.SlashCommandNotImplemented -> {
is RoomDetailViewEvents.SlashCommandNotImplemented -> {
displayCommandError(getString(R.string.not_implemented))
}
}
} // .exhaustive
lockSendButton = false
}
@ -793,84 +786,81 @@ class RoomDetailFragment @Inject constructor(
.show()
}
private fun displayRoomDetailActionResult(result: Async<RoomDetailAction>) {
when (result) {
is Fail -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(result.error))
.setPositiveButton(R.string.ok, null)
.show()
}
is Success -> {
when (val data = result.invoke()) {
is RoomDetailAction.ReportContent -> {
when {
data.spam -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_as_spam_title)
.setMessage(R.string.content_reported_as_spam_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
}
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
data.inappropriate -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_as_inappropriate_title)
.setMessage(R.string.content_reported_as_inappropriate_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
}
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
else -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_title)
.setMessage(R.string.content_reported_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
}
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
}
private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(result.throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
private fun displayRoomDetailActionSuccess(result: RoomDetailViewEvents.ActionSuccess) {
when (val data = result.action) {
is RoomDetailAction.ReportContent -> {
when {
data.spam -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_as_spam_title)
.setMessage(R.string.content_reported_as_spam_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
}
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
is RoomDetailAction.RequestVerification -> {
Timber.v("## SAS RequestVerification action")
VerificationBottomSheet.withArgs(
roomDetailArgs.roomId,
data.userId
).show(parentFragmentManager, "REQ")
data.inappropriate -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_as_inappropriate_title)
.setMessage(R.string.content_reported_as_inappropriate_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
}
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
is RoomDetailAction.AcceptVerificationRequest -> {
Timber.v("## SAS AcceptVerificationRequest action")
VerificationBottomSheet.withArgs(
roomDetailArgs.roomId,
data.otherUserId,
data.transactionId
).show(parentFragmentManager, "REQ")
}
is RoomDetailAction.ResumeVerification -> {
val otherUserId = data.otherUserId ?: return
VerificationBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(
otherUserId, data.transactionId, roomId = roomDetailArgs.roomId))
}
}.show(parentFragmentManager, "REQ")
else -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_title)
.setMessage(R.string.content_reported_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
}
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
}
}
is RoomDetailAction.RequestVerification -> {
Timber.v("## SAS RequestVerification action")
VerificationBottomSheet.withArgs(
roomDetailArgs.roomId,
data.userId
).show(parentFragmentManager, "REQ")
}
is RoomDetailAction.AcceptVerificationRequest -> {
Timber.v("## SAS AcceptVerificationRequest action")
VerificationBottomSheet.withArgs(
roomDetailArgs.roomId,
data.otherUserId,
data.transactionId
).show(parentFragmentManager, "REQ")
}
is RoomDetailAction.ResumeVerification -> {
val otherUserId = data.otherUserId ?: return
VerificationBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(
otherUserId, data.transactionId, roomId = roomDetailArgs.roomId))
}
}.show(parentFragmentManager, "REQ")
}
}
}
// TimelineEventController.Callback ************************************************************
// TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String): Boolean {
permalinkHandler

View File

@ -16,7 +16,10 @@
package im.vector.riotx.features.home.room.detail
import androidx.annotation.StringRes
import im.vector.riotx.core.platform.VectorViewEvents
import im.vector.riotx.features.command.Command
import java.io.File
/**
* Transient events for RoomDetail
@ -24,4 +27,34 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class RoomDetailViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : RoomDetailViewEvents()
data class OnNewTimelineEvents(val eventIds: List<String>) : RoomDetailViewEvents()
data class ActionSuccess(val action: RoomDetailAction) : RoomDetailViewEvents()
data class ActionFailure(val action: RoomDetailAction, val throwable: Throwable) : RoomDetailViewEvents()
data class ShowMessage(val message: String) : RoomDetailViewEvents()
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
data class FileTooBigError(
val filename: String,
val fileSizeInBytes: Long,
val homeServerLimitInBytes: Long
) : RoomDetailViewEvents()
data class DownloadFileState(
val mimeType: String,
val file: File?,
val throwable: Throwable?
) : RoomDetailViewEvents()
abstract class SendMessageResult : RoomDetailViewEvents()
object MessageSent : SendMessageResult()
class SlashCommandError(val command: Command) : SendMessageResult()
class SlashCommandUnknown(val command: String) : SendMessageResult()
data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult()
object SlashCommandResultOk : SendMessageResult()
class SlashCommandResultError(val throwable: Throwable) : SendMessageResult()
// TODO Remove
object SlashCommandNotImplemented : SendMessageResult()
}

View File

@ -18,10 +18,6 @@ package im.vector.riotx.features.home.room.detail
import android.net.Uri
import androidx.annotation.IdRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
@ -32,6 +28,7 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
@ -61,17 +58,14 @@ import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.core.utils.LiveEvent
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.riotx.core.utils.subscribeLogError
import im.vector.riotx.features.command.CommandParser
import im.vector.riotx.features.command.ParsedCommand
import im.vector.riotx.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.riotx.features.crypto.verification.supportedVerificationMethods
import im.vector.riotx.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.riotx.features.home.room.typing.TypingHelper
import im.vector.riotx.features.settings.VectorPreferences
@ -116,11 +110,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
var timeline = room.createTimeline(eventId, timelineSettings)
private set
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<RoomDetailAction>>>()
val requestLiveData: LiveData<LiveEvent<Async<RoomDetailAction>>>
get() = _requestLiveData
// Slot to keep a pending action during permission request
var pendingAction: RoomDetailAction? = null
// Slot to keep a pending uri during permission request
@ -320,27 +309,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}
// TODO Cleanup this and use ViewEvents
private val _nonBlockingPopAlert = MutableLiveData<LiveEvent<Pair<Int, List<Any>>>>()
val nonBlockingPopAlert: LiveData<LiveEvent<Pair<Int, List<Any>>>>
get() = _nonBlockingPopAlert
private val _sendMessageResultLiveData = MutableLiveData<LiveEvent<SendMessageResult>>()
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
get() = _sendMessageResultLiveData
private val _navigateToEvent = MutableLiveData<LiveEvent<String>>()
val navigateToEvent: LiveData<LiveEvent<String>>
get() = _navigateToEvent
private val _fileTooBigEvent = MutableLiveData<LiveEvent<FileTooBigError>>()
val fileTooBigEvent: LiveData<LiveEvent<FileTooBigError>>
get() = _fileTooBigEvent
private val _downloadedFileEvent = MutableLiveData<LiveEvent<DownloadFileState>>()
val downloadedFileEvent: LiveData<LiveEvent<DownloadFileState>>
get() = _downloadedFileEvent
fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) {
R.id.clear_message_queue ->
/* For now always disable on production, worker cancellation is not working properly */
@ -360,17 +328,17 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
is ParsedCommand.ErrorSyntax -> {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))
_viewEvents.post(RoomDetailViewEvents.SlashCommandError(slashCommandResult.command))
}
is ParsedCommand.ErrorEmptySlashCommand -> {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandUnknown("/"))
_viewEvents.post(RoomDetailViewEvents.SlashCommandUnknown("/"))
}
is ParsedCommand.ErrorUnknownSlashCommand -> {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))
_viewEvents.post(RoomDetailViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand))
}
is ParsedCommand.Invite -> {
handleInviteSlashCommand(slashCommandResult)
@ -378,55 +346,55 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
is ParsedCommand.SetUserPowerLevel -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.ClearScalarToken -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.SetMarkdown -> {
vectorPreferences.setMarkdownEnabled(slashCommandResult.enable)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled(
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled(
if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
popDraft()
}
is ParsedCommand.UnbanUser -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.BanUser -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.KickUser -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.JoinRoom -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.PartRoom -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.SendEmote -> {
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendRainbow -> {
slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
}
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendRainbowEmote -> {
slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
}
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendSpoiler -> {
@ -434,7 +402,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
"[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})",
"<span data-mx-spoiler>${slashCommandResult.message}</span>"
)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendShrug -> {
@ -446,12 +414,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}
room.sendTextMessage(sequence)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.VerifyUser -> {
session.getVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, slashCommandResult.userId, room.roomId)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.ChangeTopic -> {
@ -460,7 +428,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
is ParsedCommand.ChangeDisplayName -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
}.exhaustive
}
@ -487,7 +455,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
Timber.w("Same message content, do not send edition")
}
}
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
is SendMode.QUOTE -> {
@ -510,13 +478,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} else {
room.sendFormattedTextMessage(finalText, htmlText)
}
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
is SendMode.REPLY -> {
state.sendMode.timelineEvent.let {
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
}
@ -549,29 +517,29 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
room.updateTopic(changeTopic.topic, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk)
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultOk)
}
override fun onFailure(failure: Throwable) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultError(failure))
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
}
})
}
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
room.invite(invite.userId, invite.reason, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk)
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultOk)
}
override fun onFailure(failure: Throwable) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultError(failure))
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
}
})
}
@ -608,8 +576,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} else {
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
null -> room.sendMedias(attachments)
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name
?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError(
tooBigFile.name ?: tooBigFile.path,
tooBigFile.size,
maxUploadFileSize
))
}
}
}
@ -721,7 +692,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
object : MatrixCallback<File> {
override fun onSuccess(data: File) {
_downloadedFileEvent.postLiveEvent(DownloadFileState(
_viewEvents.post(RoomDetailViewEvents.DownloadFileState(
action.messageFileContent.getMimeType(),
data,
null
@ -729,7 +700,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
override fun onFailure(failure: Throwable) {
_downloadedFileEvent.postLiveEvent(DownloadFileState(
_viewEvents.post(RoomDetailViewEvents.DownloadFileState(
action.messageFileContent.getMimeType(),
null,
failure
@ -750,7 +721,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
if (action.highlight) {
setState { copy(highlightedEventId = correctedEventId) }
}
_navigateToEvent.postLiveEvent(correctedEventId)
_viewEvents.post(RoomDetailViewEvents.NavigateToEvent(correctedEventId))
}
private fun handleResendEvent(action: RoomDetailAction.ResendMessage) {
@ -821,11 +792,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleReportContent(action: RoomDetailAction.ReportContent) {
room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_requestLiveData.postValue(LiveEvent(Success(action)))
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
}
override fun onFailure(failure: Throwable) {
_requestLiveData.postValue(LiveEvent(Fail(failure)))
_viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
}
})
}
@ -837,11 +808,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
session.ignoreUserIds(listOf(action.userId), object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_requestLiveData.postValue(LiveEvent(Success(action)))
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
}
override fun onFailure(failure: Throwable) {
_requestLiveData.postValue(LiveEvent(Fail(failure)))
_viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
}
})
}
@ -853,7 +824,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
action.otherUserId,
room.roomId,
action.transactionId)) {
_requestLiveData.postValue(LiveEvent(Success(action)))
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
} else {
// TODO
}
@ -869,7 +840,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) {
if (action.userId == session.myUserId) return
_requestLiveData.postValue(LiveEvent(Success(action)))
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
}
private fun handleResumeRequestVerification(action: RoomDetailAction.ResumeVerification) {
@ -877,9 +848,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
session.getVerificationService().getExistingVerificationRequestInRoom(room.roomId, action.transactionId)?.let {
if (it.handledByOtherSession) return
if (!it.isFinished) {
_requestLiveData.postValue(LiveEvent(Success(action.copy(
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action.copy(
otherUserId = it.otherUserId
))))
)))
}
}
}

View File

@ -1,31 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail
import androidx.annotation.StringRes
import im.vector.riotx.features.command.Command
sealed class SendMessageResult {
object MessageSent : SendMessageResult()
class SlashCommandError(val command: Command) : SendMessageResult()
class SlashCommandUnknown(val command: String) : SendMessageResult()
data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult()
object SlashCommandResultOk : SendMessageResult()
class SlashCommandResultError(val throwable: Throwable) : SendMessageResult()
// TODO Remove
object SlashCommandNotImplemented : SendMessageResult()
}

View File

@ -28,7 +28,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseFragment
import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.fragment_public_rooms.*
@ -75,13 +75,20 @@ class PublicRoomsFragment @Inject constructor(
sharedActionViewModel.post(RoomDirectorySharedAction.CreateRoom)
}
// TODO remove this, replace by ViewEvents
viewModel.joinRoomErrorLiveData.observeEvent(this) { throwable ->
Snackbar.make(publicRoomsCoordinator, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT)
.show()
viewModel.observeViewEvents {
handleViewEvents(it)
}
}
private fun handleViewEvents(viewEvents: RoomDirectoryViewEvents) {
when (viewEvents) {
is RoomDirectoryViewEvents.Failure -> {
Snackbar.make(publicRoomsCoordinator, errorFormatter.toHumanReadable(viewEvents.throwable), Snackbar.LENGTH_SHORT)
.show()
}
}.exhaustive
}
override fun onDestroyView() {
publicRoomsController.callback = null
publicRoomsList.cleanup()

View File

@ -1,11 +1,11 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* 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,
@ -14,10 +14,13 @@
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail
package im.vector.riotx.features.roomdirectory
data class FileTooBigError(
val filename: String,
val fileSizeInBytes: Long,
val homeServerLimitInBytes: Long
)
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for room directory screen
*/
sealed class RoomDirectoryViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : RoomDirectoryViewEvents()
}

View File

@ -16,9 +16,13 @@
package im.vector.riotx.features.roomdirectory
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.*
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.airbnb.mvrx.appendAt
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
@ -32,17 +36,14 @@ import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryD
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import timber.log.Timber
private const val PUBLIC_ROOMS_LIMIT = 20
class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: PublicRoomsViewState,
private val session: Session)
: VectorViewModel<PublicRoomsViewState, RoomDirectoryAction, EmptyViewEvents>(initialState) {
: VectorViewModel<PublicRoomsViewState, RoomDirectoryAction, RoomDirectoryViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -58,10 +59,6 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
}
}
private val _joinRoomErrorLiveData = MutableLiveData<LiveEvent<Throwable>>()
val joinRoomErrorLiveData: LiveData<LiveEvent<Throwable>>
get() = _joinRoomErrorLiveData
private var since: String? = null
private var currentTask: Cancelable? = null
@ -109,9 +106,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
override fun handle(action: RoomDirectoryAction) {
when (action) {
is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action)
is RoomDirectoryAction.FilterWith -> filterWith(action)
RoomDirectoryAction.LoadMore -> loadMore()
is RoomDirectoryAction.JoinRoom -> joinRoom(action)
is RoomDirectoryAction.FilterWith -> filterWith(action)
RoomDirectoryAction.LoadMore -> loadMore()
is RoomDirectoryAction.JoinRoom -> joinRoom(action)
}
}
@ -227,7 +224,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
override fun onFailure(failure: Throwable) {
// Notify the user
_joinRoomErrorLiveData.postLiveEvent(failure)
_viewEvents.post(RoomDirectoryViewEvents.Failure(failure))
setState {
copy(

View File

@ -20,8 +20,7 @@ package im.vector.riotx.features.roommemberprofile
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomMemberProfileAction : VectorViewModelAction {
object RetryFetchingInfo: RoomMemberProfileAction()
object IgnoreUser: RoomMemberProfileAction()
data class VerifyUser(val userId: String? = null, val roomId: String? = null, val canCrossSign: Boolean? = true): RoomMemberProfileAction()
object RetryFetchingInfo : RoomMemberProfileAction()
object IgnoreUser : RoomMemberProfileAction()
object VerifyUser : RoomMemberProfileAction()
}

View File

@ -35,7 +35,6 @@ import im.vector.riotx.core.animations.MatrixItemAppBarStateChangeListener
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment
@ -94,33 +93,27 @@ class RoomMemberProfileFragment @Inject constructor(
is RoomMemberProfileViewEvents.Loading -> showLoading(it.message)
is RoomMemberProfileViewEvents.Failure -> showFailure(it.throwable)
is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit
is RoomMemberProfileViewEvents.StartVerification -> handleStartVerification(it)
}.exhaustive
}
viewModel.actionResultLiveData.observeEvent(this) { async ->
when (async) {
is Success -> {
when (val action = async.invoke()) {
is RoomMemberProfileAction.VerifyUser -> {
if (action.canCrossSign == true) {
VerificationBottomSheet
.withArgs(roomId = null, otherUserId = action.userId!!)
.show(parentFragmentManager, "VERIF")
} else {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_warning)
.setMessage(R.string.verify_cannot_cross_sign)
.setPositiveButton(R.string.verification_profile_verify) { _, _ ->
VerificationBottomSheet
.withArgs(roomId = null, otherUserId = action.userId!!)
.show(parentFragmentManager, "VERIF")
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
}
private fun handleStartVerification(startVerification: RoomMemberProfileViewEvents.StartVerification) {
if (startVerification.canCrossSign) {
VerificationBottomSheet
.withArgs(roomId = null, otherUserId = startVerification.userId)
.show(parentFragmentManager, "VERIF")
} else {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_warning)
.setMessage(R.string.verify_cannot_cross_sign)
.setPositiveButton(R.string.verification_profile_verify) { _, _ ->
VerificationBottomSheet
.withArgs(roomId = null, otherUserId = startVerification.userId)
.show(parentFragmentManager, "VERIF")
}
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
@ -197,7 +190,7 @@ class RoomMemberProfileFragment @Inject constructor(
}
override fun onTapVerify() {
viewModel.handle(RoomMemberProfileAction.VerifyUser())
viewModel.handle(RoomMemberProfileAction.VerifyUser)
}
override fun onShowDeviceList() = withState(viewModel) {

View File

@ -26,4 +26,9 @@ sealed class RoomMemberProfileViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : RoomMemberProfileViewEvents()
object OnIgnoreActionSuccess : RoomMemberProfileViewEvents()
data class StartVerification(
val userId: String,
val canCrossSign: Boolean
) : RoomMemberProfileViewEvents()
}

View File

@ -17,10 +17,7 @@
package im.vector.riotx.features.roommemberprofile
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
@ -49,7 +46,6 @@ import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import kotlinx.coroutines.Dispatchers
@ -75,10 +71,6 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
}
}
private val _actionResultLiveData = MutableLiveData<LiveEvent<Async<RoomMemberProfileAction>>>()
val actionResultLiveData: LiveData<LiveEvent<Async<RoomMemberProfileAction>>>
get() = _actionResultLiveData
private val room = if (initialState.roomId != null) {
session.getRoom(initialState.roomId)
} else {
@ -145,23 +137,19 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
when (action) {
is RoomMemberProfileAction.RetryFetchingInfo -> fetchProfileInfo()
is RoomMemberProfileAction.IgnoreUser -> handleIgnoreAction()
is RoomMemberProfileAction.VerifyUser -> prepareVerification(action)
is RoomMemberProfileAction.VerifyUser -> prepareVerification()
}
}
private fun prepareVerification(action: RoomMemberProfileAction.VerifyUser) = withState { state ->
private fun prepareVerification() = withState { state ->
// Sanity
if (state.isRoomEncrypted) {
if (!state.isMine && state.userMXCrossSigningInfo?.isTrusted() == false) {
// ok, let's find or create the DM room
_actionResultLiveData.postValue(
LiveEvent(Success(
action.copy(
userId = state.userId,
canCrossSign = session.getCrossSigningService().canCrossSign()
)
))
)
_viewEvents.post(RoomMemberProfileViewEvents.StartVerification(
userId = state.userId,
canCrossSign = session.getCrossSigningService().canCrossSign()
))
}
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.roommemberprofile.devices
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class DeviceListAction : VectorViewModelAction {
data class SelectDevice(val device: CryptoDeviceInfo) : DeviceListAction()
object DeselectDevice : DeviceListAction()
data class ManuallyVerify(val deviceId: String) : DeviceListAction()
}

View File

@ -21,15 +21,13 @@ import android.os.Bundle
import android.view.KeyEvent
import androidx.fragment.app.Fragment
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.commitTransaction
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.crypto.verification.VerificationAction
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import javax.inject.Inject
import kotlin.reflect.KClass
@ -48,20 +46,16 @@ class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.requestLiveData.observeEvent(this) { async ->
when (async) {
is Success -> {
when (val action = async.invoke()) {
is VerificationAction.StartSASVerification -> {
VerificationBottomSheet.withArgs(
roomId = null,
otherUserId = action.otherUserId,
transactionId = action.pendingRequestTransactionId
).show(requireActivity().supportFragmentManager, "REQPOP")
}
}
viewModel.observeViewEvents {
when (it) {
is DeviceListBottomSheetViewEvents.Verify -> {
VerificationBottomSheet.withArgs(
roomId = null,
otherUserId = it.userId,
transactionId = it.txID
).show(requireActivity().supportFragmentManager, "REQPOP")
}
}
}.exhaustive
}
}
@ -69,7 +63,7 @@ class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() {
withState(viewModel) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (it.selectedDevice != null) {
viewModel.selectDevice(null)
viewModel.handle(DeviceListAction.DeselectDevice)
return@withState true
} else {
return@withState false

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.roommemberprofile.devices
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for device list screen
*/
sealed class DeviceListBottomSheetViewEvents : VectorViewEvents {
data class Verify(val userId: String, val txID: String) : DeviceListBottomSheetViewEvents()
}

View File

@ -16,14 +16,11 @@
*/
package im.vector.riotx.features.roommemberprofile.devices
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
@ -35,12 +32,8 @@ import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.rx.rx
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.features.crypto.verification.VerificationAction
data class DeviceListViewState(
val userItem: MatrixItem? = null,
@ -52,14 +45,8 @@ data class DeviceListViewState(
class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState,
@Assisted private val userId: String,
private val stringProvider: StringProvider,
private val session: Session)
: VectorViewModel<DeviceListViewState, EmptyAction, EmptyViewEvents>(initialState) {
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<VerificationAction>>>()
val requestLiveData: LiveData<LiveEvent<Async<VerificationAction>>>
get() = _requestLiveData
: VectorViewModel<DeviceListViewState, DeviceListAction, DeviceListBottomSheetViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -67,7 +54,6 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
}
init {
session.rx().liveUserCryptoDevices(userId)
.execute {
copy(cryptoDevices = it).also {
@ -81,6 +67,14 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
}
}
override fun handle(action: DeviceListAction) {
when (action) {
is DeviceListAction.SelectDevice -> selectDevice(action)
is DeviceListAction.DeselectDevice -> deselectDevice()
is DeviceListAction.ManuallyVerify -> manuallyVerify(action)
}.exhaustive
}
private fun refreshSelectedId() = withState { state ->
if (state.selectedDevice != null) {
state.cryptoDevices.invoke()?.firstOrNull { state.selectedDevice.deviceId == it.deviceId }?.let {
@ -93,19 +87,23 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
}
}
fun selectDevice(device: CryptoDeviceInfo?) {
private fun selectDevice(action: DeviceListAction.SelectDevice) {
setState {
copy(selectedDevice = device)
copy(selectedDevice = action.device)
}
}
fun manuallyVerify(device: CryptoDeviceInfo) {
session.getVerificationService().beginKeyVerification(VerificationMethod.SAS, userId, device.deviceId, null)?.let { txID ->
_requestLiveData.postValue(LiveEvent(Success(VerificationAction.StartSASVerification(userId, txID))))
private fun deselectDevice() {
setState {
copy(selectedDevice = null)
}
}
override fun handle(action: EmptyAction) {}
private fun manuallyVerify(action: DeviceListAction.ManuallyVerify) {
session.getVerificationService().beginKeyVerification(VerificationMethod.SAS, userId, action.deviceId, null)?.let { txID ->
_viewEvents.post(DeviceListBottomSheetViewEvents.Verify(userId, txID))
}
}
companion object : MvRxViewModelFactory<DeviceListBottomSheetViewModel, DeviceListViewState> {
@JvmStatic

View File

@ -62,6 +62,6 @@ class DeviceListFragment @Inject constructor(
}
override fun onDeviceSelected(device: CryptoDeviceInfo) {
viewModel.selectDevice(device)
viewModel.handle(DeviceListAction.SelectDevice(device))
}
}

View File

@ -62,6 +62,6 @@ class DeviceTrustInfoActionFragment @Inject constructor(
}
override fun onVerifyManually(device: CryptoDeviceInfo) {
viewModel.manuallyVerify(device)
viewModel.handle(DeviceListAction.ManuallyVerify(device.deviceId))
}
}

View File

@ -18,16 +18,13 @@ package im.vector.riotx.features.settings.crosssigning
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.PromptPasswordDialog
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
@ -44,23 +41,20 @@ class CrossSigningSettingsFragment @Inject constructor(
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.requestLiveData.observeEvent(this) {
viewModel.observeViewEvents {
when (it) {
is Fail -> {
is CrossSigningSettingsViewEvents.Failure -> {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_error)
.setMessage(it.error.message)
.setMessage(errorFormatter.toHumanReadable(it.throwable))
.setPositiveButton(R.string.ok, null)
.show()
Unit
}
is Success -> {
when (val action = it.invoke()) {
is CrossSigningAction.RequestPasswordAuth -> {
requestPassword(action.sessionId)
}
}
is CrossSigningSettingsViewEvents.RequestPassword -> {
requestPassword()
}
}
}.exhaustive
}
}
@ -89,18 +83,14 @@ class CrossSigningSettingsFragment @Inject constructor(
super.onDestroyView()
}
private fun requestPassword(sessionId: String) {
private fun requestPassword() {
PromptPasswordDialog().show(requireActivity()) { password ->
// TODO sessionId should never get out the ViewModel
viewModel.handle(CrossSigningAction.InitializeCrossSigning(UserPasswordAuth(
session = sessionId,
password = password
)))
viewModel.handle(CrossSigningAction.PasswordEntered(password))
}
}
override fun onInitializeCrossSigningKeys() {
viewModel.handle(CrossSigningAction.InitializeCrossSigning())
viewModel.handle(CrossSigningAction.InitializeCrossSigning)
}
override fun onResetCrossSigningKeys() {
@ -108,7 +98,7 @@ class CrossSigningSettingsFragment @Inject constructor(
.setTitle(R.string.dialog_title_confirmation)
.setMessage(R.string.are_you_sure)
.setPositiveButton(R.string.ok) { _, _ ->
viewModel.handle(CrossSigningAction.InitializeCrossSigning())
viewModel.handle(CrossSigningAction.InitializeCrossSigning)
}
.show()
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.crosssigning
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for cross signing settings screen
*/
sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
object RequestPassword : CrossSigningSettingsViewEvents()
}

View File

@ -15,14 +15,9 @@
*/
package im.vector.riotx.features.settings.crosssigning
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
@ -36,11 +31,9 @@ import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
data class CrossSigningSettingsViewState(
val crossSigningInfo: MXCrossSigningInfo? = null,
@ -51,19 +44,13 @@ data class CrossSigningSettingsViewState(
) : MvRxState
sealed class CrossSigningAction : VectorViewModelAction {
data class InitializeCrossSigning(val auth: UserPasswordAuth? = null) : CrossSigningAction()
data class RequestPasswordAuth(val sessionId: String) : CrossSigningAction()
object InitializeCrossSigning : CrossSigningAction()
data class PasswordEntered(val password: String) : CrossSigningAction()
}
class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState,
private val stringProvider: StringProvider,
private val session: Session)
: VectorViewModel<CrossSigningSettingsViewState, CrossSigningAction, EmptyViewEvents>(initialState) {
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<CrossSigningAction>>>()
val requestLiveData: LiveData<LiveEvent<Async<CrossSigningAction>>>
get() = _requestLiveData
: VectorViewModel<CrossSigningSettingsViewState, CrossSigningAction, CrossSigningSettingsViewEvents>(initialState) {
init {
session.rx().liveCrossSigningInfo(session.myUserId)
@ -81,6 +68,9 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
}
}
// Storage when password is required
private var _pendingSession: String? = null
@AssistedInject.Factory
interface Factory {
fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel
@ -89,26 +79,37 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
override fun handle(action: CrossSigningAction) {
when (action) {
is CrossSigningAction.InitializeCrossSigning -> {
initializeCrossSigning(action.auth?.copy(user = session.myUserId))
initializeCrossSigning(null)
}
}
is CrossSigningAction.PasswordEntered -> {
initializeCrossSigning(UserPasswordAuth(
session = _pendingSession,
user = session.myUserId,
password = action.password
))
}
}.exhaustive
}
private fun initializeCrossSigning(auth: UserPasswordAuth?) {
_pendingSession = null
setState {
copy(isUploadingKeys = true)
}
session.getCrossSigningService().initializeCrossSigning(auth, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_pendingSession = null
setState {
copy(isUploadingKeys = false)
}
}
override fun onFailure(failure: Throwable) {
if (failure is Failure.OtherServerError
&& failure.httpCode == 401
) {
_pendingSession = null
if (failure is Failure.OtherServerError && failure.httpCode == 401) {
try {
MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
@ -118,23 +119,23 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
}?.let { flowResponse ->
// Retry with authentication
if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) {
_requestLiveData.postValue(LiveEvent(Success(CrossSigningAction.RequestPasswordAuth(flowResponse.session ?: ""))))
_pendingSession = flowResponse.session ?: ""
_viewEvents.post(CrossSigningSettingsViewEvents.RequestPassword)
return
} else {
_requestLiveData.postValue(LiveEvent(Fail(Throwable("You cannot do that from mobile"))))
// can't do this from here
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(Throwable("You cannot do that from mobile")))
setState {
copy(isUploadingKeys = false)
}
return
}
}
}
when (failure) {
is Failure.ServerError -> {
_requestLiveData.postValue(LiveEvent(Fail(Throwable(failure.error.message))))
}
else -> {
_requestLiveData.postValue(LiveEvent(Fail(failure)))
}
}
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(failure))
setState {
copy(isUploadingKeys = false)
}

View File

@ -16,14 +16,14 @@
package im.vector.riotx.features.settings.devices
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class DevicesAction : VectorViewModelAction {
object Retry : DevicesAction()
data class Delete(val deviceId: String) : DevicesAction()
data class Password(val password: String) : DevicesAction()
data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction()
data class PromptRename(val deviceId: String, val deviceInfo: DeviceInfo? = null) : DevicesAction()
data class VerifyMyDevice(val deviceId: String, val userId: String? = null, val transactionId: String? = null) : DevicesAction()
data class Rename(val deviceId: String, val newName: String) : DevicesAction()
data class PromptRename(val deviceId: String) : DevicesAction()
data class VerifyMyDevice(val deviceId: String) : DevicesAction()
}

View File

@ -17,6 +17,7 @@
package im.vector.riotx.features.settings.devices
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.core.platform.VectorViewEvents
/**
@ -25,4 +26,13 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class DevicesViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : DevicesViewEvents()
data class Failure(val throwable: Throwable) : DevicesViewEvents()
object RequestPassword : DevicesViewEvents()
data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvents()
data class ShowVerifyDevice(
val userId: String,
val transactionId: String?
) : DevicesViewEvents()
}

View File

@ -16,8 +16,6 @@
package im.vector.riotx.features.settings.devices
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
@ -41,9 +39,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.features.crypto.verification.supportedVerificationMethods
data class DevicesViewState(
@ -76,15 +72,6 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
private var _currentDeviceId: String? = null
private var _currentSession: String? = null
private val _requestPasswordLiveData = MutableLiveData<LiveEvent<Unit>>()
val requestPasswordLiveData: LiveData<LiveEvent<Unit>>
get() = _requestPasswordLiveData
// Used to communicate back from model to fragment
private val _requestLiveData = MutableLiveData<LiveEvent<Async<DevicesAction>>>()
val fragmentActionLiveData: LiveData<LiveEvent<Async<DevicesAction>>>
get() = _requestLiveData
init {
refreshDevicesList()
session.getVerificationService().addListener(this)
@ -187,25 +174,21 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
private fun handleVerify(action: DevicesAction.VerifyMyDevice) {
val txID = session.getVerificationService().requestKeyVerification(supportedVerificationMethods, session.myUserId, listOf(action.deviceId))
_requestLiveData.postValue(LiveEvent(Success(
action.copy(
userId = session.myUserId,
transactionId = txID.transactionId
)
)))
_viewEvents.post(DevicesViewEvents.ShowVerifyDevice(
session.myUserId,
txID.transactionId
))
}
private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state ->
val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId }
if (info == null) {
_requestLiveData.postValue(LiveEvent(Uninitialized))
} else {
_requestLiveData.postValue(LiveEvent(Success(action.copy(deviceInfo = info))))
if (info != null) {
_viewEvents.post(DevicesViewEvents.PromptRenameDevice(info))
}
}
private fun handleRename(action: DevicesAction.Rename) {
session.setDeviceName(action.deviceInfo.deviceId!!, action.newName, object : MatrixCallback<Unit> {
session.setDeviceName(action.deviceId, action.newName, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
setState {
copy(
@ -261,7 +244,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
)
}
_requestPasswordLiveData.postLiveEvent(Unit)
_viewEvents.post(DevicesViewEvents.RequestPassword)
}
}

View File

@ -23,7 +23,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
@ -32,7 +31,6 @@ import im.vector.riotx.core.dialogs.PromptPasswordDialog
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
@ -64,35 +62,18 @@ class VectorSettingsDevicesFragment @Inject constructor(
recyclerView.configureWith(devicesController, showDivider = true)
viewModel.observeViewEvents {
when (it) {
is DevicesViewEvents.Loading -> showLoading(it.message)
is DevicesViewEvents.Failure -> showFailure(it.throwable)
}.exhaustive
}
viewModel.requestPasswordLiveData.observeEvent(this) {
maybeShowDeleteDeviceWithPasswordDialog()
}
viewModel.fragmentActionLiveData.observeEvent(this) { async ->
when (async) {
is Success -> {
when (val action = async.invoke()) {
is DevicesAction.PromptRename -> {
action.deviceInfo?.let { deviceInfo ->
displayDeviceRenameDialog(deviceInfo)
}
}
is DevicesAction.VerifyMyDevice -> {
if (context is VectorBaseActivity) {
VerificationBottomSheet.withArgs(
roomId = null,
otherUserId = action.userId!!,
transactionId = action.transactionId!!
).show(childFragmentManager, "REQPOP")
}
}
}
is DevicesViewEvents.Loading -> showLoading(it.message)
is DevicesViewEvents.Failure -> showFailure(it.throwable)
is DevicesViewEvents.RequestPassword -> maybeShowDeleteDeviceWithPasswordDialog()
is DevicesViewEvents.PromptRenameDevice -> displayDeviceRenameDialog(it.deviceInfo)
is DevicesViewEvents.ShowVerifyDevice -> {
VerificationBottomSheet.withArgs(
roomId = null,
otherUserId = it.userId,
transactionId = it.transactionId
).show(childFragmentManager, "REQPOP")
}
}
}.exhaustive
}
}
@ -152,7 +133,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
.setPositiveButton(R.string.ok) { _, _ ->
val newName = input.text.toString()
viewModel.handle(DevicesAction.Rename(deviceInfo, newName))
viewModel.handle(DevicesAction.Rename(deviceInfo.deviceId!!, newName))
}
.setNegativeButton(R.string.cancel, null)
.show()