Merge branch 'develop' into cross_signing

This commit is contained in:
Valere 2020-01-28 11:13:31 +01:00
commit 7daa088618
81 changed files with 1152 additions and 371 deletions

View File

@ -9,6 +9,8 @@
<w>decryptor</w>
<w>emoji</w>
<w>emojis</w>
<w>fdroid</w>
<w>gplay</w>
<w>hmac</w>
<w>ktlint</w>
<w>linkified</w>

View File

@ -0,0 +1,35 @@
A full developer contributors list can be found [here](https://github.com/vector-im/riotX-android/graphs/contributors).
# Core team:
Even if we try to be able to work on all the functionalities, we have more knowledge about what we have developed ourselves.
## Benoit: Android team leader
[@benoit.marty:matrix.org](https://matrix.to/#/@benoit.marty:matrix.org)
- Android team leader and project leader, Android developer, GitHub community manager.
- Specialist of the account creation, and many other fun features.
- Reviewing and polishing developed features, code quality manager, PRs reviewer, GitHub community manager.
- Release manager on the Play Store
## François: Software architect
[@ganfra:matrix.org](https://matrix.to/#/@ganfra:matrix.org)
- Software architect, Android developer
- First developer on the project.
- Work mainly on the global architecture of the project.
- Specialist of the timeline, and lots of other features.
## Valere: Product manager, Android developer
[@valere35:matrix.org](https://matrix.to/#/@valere35:matrix.org)
- Product manager, Android developer
- Specialist on the crypto implementation.
# Other contributors
First of all, we thank all contributors who use RiotX and report problems on this GitHub project or via the integrated rageshake function.
We do not forget all translators, for their work of translating RiotX into many languages. They are also the authors of RiotX.
Feel free to add your name below, when you contribute to the project!

View File

@ -2,10 +2,10 @@ Changes in RiotX 0.14.0 (2020-XX-XX)
===================================================
Features ✨:
-
- Enable encryption in unencrypted rooms, from the room settings (#212)
Improvements 🙌:
-
- Sharing things to RiotX: sort list by recent room first (#771)
Other changes:
-
@ -17,7 +17,8 @@ Translations 🗣:
-
Build 🧱:
-
- Ensure builds are reproducible (#842)
- F-Droid: fix the "-dev" issue in version name (#815)
Changes in RiotX 0.13.0 (2020-01-17)
===================================================

View File

@ -17,7 +17,7 @@ Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f6
# New Android SDK
RiotX is based on a new Android SDK fully written in Kotlin (like RiotX). In order to make the early development as fast as possible, RiotX and the new SDK currently share the same git repository. We will make separate repos once the API is stable enough.
RiotX is based on a new Android SDK fully written in Kotlin (like RiotX). In order to make the early development as fast as possible, RiotX and the new SDK currently share the same git repository. We will make separate repos once the SDK is stable enough.
# Roadmap

View File

@ -74,7 +74,7 @@ android {
}
static def gitRevision() {
def cmd = "git rev-parse --short HEAD"
def cmd = "git rev-parse --short=8 HEAD"
return cmd.execute().text.trim()
}

View File

@ -45,7 +45,8 @@ data class RoomSummary(
val readMarkerId: String? = null,
val userDrafts: List<UserDraft> = emptyList(),
var isEncrypted: Boolean,
val typingRoomMemberIds: List<String> = emptyList()
val typingRoomMemberIds: List<String> = emptyList(),
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS
) {
val isVersioned: Boolean
@ -53,4 +54,8 @@ data class RoomSummary(
val hasNewMessages: Boolean
get() = notificationCount != 0
companion object {
const val NOT_IN_BREADCRUMBS = -1
}
}

View File

@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import java.util.*
import java.util.UUID
import javax.inject.Inject
internal class RoomSummaryMapper @Inject constructor(
@ -74,7 +74,8 @@ internal class RoomSummaryMapper @Inject constructor(
canonicalAlias = roomSummaryEntity.canonicalAlias,
aliases = roomSummaryEntity.aliases.toList(),
isEncrypted = roomSummaryEntity.isEncrypted,
typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList()
typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList(),
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex
)
}
}

View File

@ -17,35 +17,37 @@
package im.vector.matrix.android.internal.database.model
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.VersioningState
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var displayName: String? = "",
var avatarUrl: String? = "",
var topic: String? = "",
var latestPreviewableEvent: TimelineEventEntity? = null,
var heroes: RealmList<String> = RealmList(),
var joinedMembersCount: Int? = 0,
var invitedMembersCount: Int? = 0,
var isDirect: Boolean = false,
var directUserId: String? = null,
var otherMemberIds: RealmList<String> = RealmList(),
var notificationCount: Int = 0,
var highlightCount: Int = 0,
var readMarkerId: String? = null,
var hasUnreadMessages: Boolean = false,
var tags: RealmList<RoomTagEntity> = RealmList(),
var userDrafts: UserDraftsEntity? = null,
var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
var canonicalAlias: String? = null,
var aliases: RealmList<String> = RealmList(),
// this is required for querying
var flatAliases: String = "",
var isEncrypted: Boolean = false,
var typingUserIds: RealmList<String> = RealmList()
internal open class RoomSummaryEntity(
@PrimaryKey var roomId: String = "",
var displayName: String? = "",
var avatarUrl: String? = "",
var topic: String? = "",
var latestPreviewableEvent: TimelineEventEntity? = null,
var heroes: RealmList<String> = RealmList(),
var joinedMembersCount: Int? = 0,
var invitedMembersCount: Int? = 0,
var isDirect: Boolean = false,
var directUserId: String? = null,
var otherMemberIds: RealmList<String> = RealmList(),
var notificationCount: Int = 0,
var highlightCount: Int = 0,
var readMarkerId: String? = null,
var hasUnreadMessages: Boolean = false,
var tags: RealmList<RoomTagEntity> = RealmList(),
var userDrafts: UserDraftsEntity? = null,
var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS,
var canonicalAlias: String? = null,
var aliases: RealmList<String> = RealmList(),
// this is required for querying
var flatAliases: String = "",
var isEncrypted: Boolean = false,
var typingUserIds: RealmList<String> = RealmList()
) : RealmObject() {
private var membershipStr: String = Membership.NONE.name
@ -66,7 +68,5 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
versioningStateStr = value.name
}
companion object {
const val NOT_IN_BREADCRUMBS = -1
}
companion object
}

View File

@ -144,7 +144,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
return RoomSummaryEntity.where(realm)
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS)
.sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX)
}

View File

@ -21,16 +21,26 @@ import im.vector.matrix.android.api.pushrules.RuleScope
import im.vector.matrix.android.api.pushrules.RuleSetKey
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity
import im.vector.matrix.android.internal.database.model.IgnoredUserEntity
import im.vector.matrix.android.internal.database.model.PushRulesEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.getDirectRooms
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
import im.vector.matrix.android.internal.session.sync.model.accountdata.*
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataBreadcrumbs
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataDirectMessages
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataPushRules
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataSync
import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import io.realm.Realm
@ -177,10 +187,10 @@ internal class UserAccountDataSyncHandler @Inject constructor(
// Update the room summaries
// Reset all the indexes...
RoomSummaryEntity.where(realm)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS)
.findAll()
.forEach {
it.breadcrumbsIndex = RoomSummaryEntity.NOT_IN_BREADCRUMBS
it.breadcrumbsIndex = RoomSummary.NOT_IN_BREADCRUMBS
}
// ...and apply new indexes

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.session.user.accountdata
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
@ -50,10 +51,10 @@ internal class DefaultSaveBreadcrumbsTask @Inject constructor(
// Update the room summaries
// Reset all the indexes...
RoomSummaryEntity.where(realm)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS)
.findAll()
.forEach {
it.breadcrumbsIndex = RoomSummaryEntity.NOT_IN_BREADCRUMBS
it.breadcrumbsIndex = RoomSummary.NOT_IN_BREADCRUMBS
}
// ...and apply new indexes

View File

@ -27,7 +27,7 @@ static def generateVersionCodeFromTimestamp() {
// It's unix timestamp, minus timestamp of October 3rd 2018 (first commit date) divided by 100: It's incremented by one every 100 seconds.
// plus 20_000_000 for compatibility reason with the previous way the Version Code was computed
// Note that the result will be multiplied by 10 when adding the digit for the arch
return ((getGitTimestamp() - 1_538_524_800 ) / 100).toInteger() + 20_000_000
return ((getGitTimestamp() - 1_538_524_800) / 100).toInteger() + 20_000_000
}
def generateVersionCodeFromVersionName() {
@ -45,7 +45,7 @@ def getVersionCode() {
}
static def gitRevision() {
def cmd = "git rev-parse --short HEAD"
def cmd = "git rev-parse --short=8 HEAD"
return cmd.execute().text.trim()
}
@ -66,7 +66,8 @@ static def gitBranchName() {
}
}
static def getVersionSuffix() {
// For Google Play build, build on any other branch than master will have a "-dev" suffix
static def getGplayVersionSuffix() {
if (gitBranchName() == "master") {
return ""
} else {
@ -74,6 +75,20 @@ static def getVersionSuffix() {
}
}
static def gitTag() {
def cmd = "git describe --exact-match --tags"
return cmd.execute().text.trim()
}
// For F-Droid build, build on a not tagged commit will have a "-dev" suffix
static def getFdroidVersionSuffix() {
if (gitTag() == "") {
return "-dev"
} else {
return ""
}
}
project.android.buildTypes.all { buildType ->
buildType.javaCompileOptions.annotationProcessorOptions.arguments =
[
@ -102,8 +117,6 @@ android {
// Other branches (master, features, etc.) will have version code based on application version.
versionCode project.getVersionCode()
versionName "${versionMajor}.${versionMinor}.${versionPatch}${getVersionSuffix()}"
buildConfigField "String", "GIT_REVISION", "\"${gitRevision()}\""
resValue "string", "git_revision", "\"${gitRevision()}\""
@ -190,6 +203,8 @@ android {
gplay {
dimension "store"
versionName "${versionMajor}.${versionMinor}.${versionPatch}${getGplayVersionSuffix()}"
resValue "bool", "isGplay", "true"
buildConfigField "boolean", "ALLOW_FCM_USE", "true"
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\""
@ -199,6 +214,8 @@ android {
fdroid {
dimension "store"
versionName "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}"
resValue "bool", "isGplay", "false"
buildConfigField "boolean", "ALLOW_FCM_USE", "false"
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\""

View File

@ -22,6 +22,8 @@ import androidx.fragment.app.FragmentFactory
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
@ -30,24 +32,38 @@ import im.vector.riotx.features.crypto.verification.request.VerificationRequestF
import im.vector.riotx.features.home.HomeDetailFragment
import im.vector.riotx.features.home.HomeDrawerFragment
import im.vector.riotx.features.home.LoadingFragment
import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
import im.vector.riotx.features.grouplist.GroupListFragment
import im.vector.riotx.features.home.room.breadcrumbs.BreadcrumbsFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.login.*
import im.vector.riotx.features.login.LoginCaptchaFragment
import im.vector.riotx.features.login.LoginFragment
import im.vector.riotx.features.login.LoginGenericTextInputFormFragment
import im.vector.riotx.features.login.LoginResetPasswordFragment
import im.vector.riotx.features.login.LoginResetPasswordMailConfirmationFragment
import im.vector.riotx.features.login.LoginResetPasswordSuccessFragment
import im.vector.riotx.features.login.LoginServerSelectionFragment
import im.vector.riotx.features.login.LoginServerUrlFormFragment
import im.vector.riotx.features.login.LoginSignUpSignInSelectionFragment
import im.vector.riotx.features.login.LoginSplashFragment
import im.vector.riotx.features.login.LoginWaitForEmailFragment
import im.vector.riotx.features.login.LoginWebFragment
import im.vector.riotx.features.login.terms.LoginTermsFragment
import im.vector.riotx.features.roommemberprofile.RoomMemberProfileFragment
import im.vector.riotx.features.reactions.EmojiChooserFragment
import im.vector.riotx.features.reactions.EmojiSearchResultFragment
import im.vector.riotx.features.roomdirectory.PublicRoomsFragment
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
import im.vector.riotx.features.roommemberprofile.RoomMemberProfileFragment
import im.vector.riotx.features.roomprofile.RoomProfileFragment
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
import im.vector.riotx.features.settings.*
import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment
import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment
import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment
import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment
@ -257,6 +273,11 @@ interface FragmentModule {
@FragmentKey(RoomMemberListFragment::class)
fun bindRoomMemberListFragment(fragment: RoomMemberListFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomSettingsFragment::class)
fun bindRoomSettingsFragment(fragment: RoomSettingsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomMemberProfileFragment::class)

View File

@ -43,11 +43,14 @@ abstract class ProfileActionItem : VectorEpoxyModel<ProfileActionItem.Holder>()
@EpoxyAttribute
var destructive: Boolean = false
@EpoxyAttribute
lateinit var listener: View.OnClickListener
var listener: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.setOnClickListener(listener)
if (listener == null) {
holder.view.isClickable = false
}
holder.editable.isVisible = editable
holder.title.text = title
val tintColor = if (destructive) {

View File

@ -19,6 +19,7 @@ package im.vector.riotx.core.epoxy.profiles
import androidx.annotation.DrawableRes
import com.airbnb.epoxy.EpoxyController
import im.vector.riotx.core.epoxy.ClickListener
import im.vector.riotx.core.epoxy.dividerItem
fun EpoxyController.buildProfileSection(title: String) {
@ -37,7 +38,7 @@ fun EpoxyController.buildProfileAction(
@DrawableRes icon: Int = 0,
destructive: Boolean = false,
divider: Boolean = true,
action: () -> Unit
action: ClickListener? = null
) {
profileActionItem {
iconRes(icon)
@ -47,7 +48,7 @@ fun EpoxyController.buildProfileAction(
destructive(destructive)
title(title)
listener { _ ->
action()
action?.invoke()
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 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.core.extensions
// Trick to ensure that when block is exhaustive
val <T> T.exhaustive: T get() = this

View File

@ -22,10 +22,15 @@ import android.app.ProgressDialog
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.view.*
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.annotation.LayoutRes
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
@ -35,11 +40,13 @@ import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.MvRx
import com.bumptech.glide.util.Util.assertMainThread
import com.google.android.material.snackbar.Snackbar
import im.vector.riotx.R
import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.features.navigation.Navigator
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import timber.log.Timber
@ -120,6 +127,14 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
mUnBinder = ButterKnife.bind(this, view)
}
open fun showLoading(message: CharSequence?) {
showLoadingDialog(message)
}
open fun showFailure(throwable: Throwable) {
displayErrorDialog(throwable)
}
@CallSuper
override fun onDestroyView() {
super.onDestroyView()
@ -182,10 +197,10 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
}
}
protected fun showLoadingDialog(message: CharSequence, cancelable: Boolean = false) {
protected fun showLoadingDialog(message: CharSequence? = null, cancelable: Boolean = false) {
progress = ProgressDialog(requireContext()).apply {
setCancelable(cancelable)
setMessage(message)
setMessage(message ?: getString(R.string.please_wait))
setProgressStyle(ProgressDialog.STYLE_SPINNER)
show()
}
@ -220,6 +235,21 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
return this
}
/* ==========================================================================================
* ViewEvents
* ========================================================================================== */
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
dismissLoadingDialog()
observer(it)
}
.disposeOnDestroyView()
}
/* ==========================================================================================
* MENU MANAGEMENT
* ========================================================================================== */
@ -233,4 +263,16 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
inflater.inflate(menuRes, menu)
}
}
/* ==========================================================================================
* Common Dialogs
* ========================================================================================== */
protected fun displayErrorDialog(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 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.core.platform
/**
* Interface for View Events
*/
interface VectorViewEvents
/**
* To use when no view events is associated to the ViewModel
*/
object EmptyViewEvents : VectorViewEvents

View File

@ -16,20 +16,23 @@
package im.vector.riotx.core.platform
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.*
import im.vector.riotx.core.utils.LiveEvent
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.BaseMvRxViewModel
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Success
import im.vector.riotx.core.utils.DataSource
import im.vector.riotx.core.utils.PublishDataSource
import io.reactivex.Observable
import io.reactivex.Single
abstract class VectorViewModel<S : MvRxState, A : VectorViewModelAction>(initialState: S)
abstract class VectorViewModel<S : MvRxState, VA : VectorViewModelAction, VE : VectorViewEvents>(initialState: S)
: BaseMvRxViewModel<S>(initialState, false) {
// Generic handling of any request error
protected val _requestErrorLiveData = MutableLiveData<LiveEvent<Throwable>>()
val requestErrorLiveData: LiveData<LiveEvent<Throwable>>
get() = _requestErrorLiveData
// Used to post transient events to the View
protected val _viewEvents = PublishDataSource<VE>()
val viewEvents: DataSource<VE> = _viewEvents
/**
* This method does the same thing as the execute function, but it doesn't subscribe to the stream
@ -53,5 +56,5 @@ abstract class VectorViewModel<S : MvRxState, A : VectorViewModelAction>(initial
.doOnNext { setState { stateReducer(it) } }
}
abstract fun handle(action: A)
abstract fun handle(action: VA)
}

View File

@ -23,7 +23,11 @@ import com.airbnb.mvrx.withState
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.hideKeyboard
import im.vector.riotx.core.extensions.setupAsSearch
import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.*
import javax.inject.Inject

View File

@ -33,6 +33,7 @@ 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
@ -51,7 +52,7 @@ data class SelectUserAction(
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState,
private val session: Session)
: VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction>(initialState) {
: VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {

View File

@ -24,11 +24,12 @@ import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialState: KeysBackupSettingViewState,
session: Session
) : VectorViewModel<KeysBackupSettingViewState, KeyBackupSettingsAction>(initialState),
) : VectorViewModel<KeysBackupSettingViewState, KeyBackupSettingsAction, EmptyViewEvents>(initialState),
KeysBackupStateListener {
@AssistedInject.Factory

View File

@ -33,6 +33,7 @@ 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
@ -45,7 +46,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
private val selectedGroupStore: SelectedGroupDataSource,
private val session: Session,
private val stringProvider: StringProvider
) : VectorViewModel<GroupListViewState, GroupListAction>(initialState) {
) : VectorViewModel<GroupListViewState, GroupListAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {

View File

@ -24,6 +24,7 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx
import im.vector.riotx.core.di.HasScreenInjector
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.features.grouplist.SelectedGroupDataSource
@ -40,7 +41,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
private val selectedGroupStore: SelectedGroupDataSource,
private val homeRoomListStore: HomeRoomListDataSource,
private val stringProvider: StringProvider)
: VectorViewModel<HomeDetailViewState, HomeDetailAction>(initialState) {
: VectorViewModel<HomeDetailViewState, HomeDetailAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {

View File

@ -24,12 +24,13 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import io.reactivex.schedulers.Schedulers
class BreadcrumbsViewModel @AssistedInject constructor(@Assisted initialState: BreadcrumbsViewState,
private val session: Session)
: VectorViewModel<BreadcrumbsViewState, EmptyAction>(initialState) {
: VectorViewModel<BreadcrumbsViewState, EmptyAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {

View File

@ -87,6 +87,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.dialogs.withColoredButton
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
@ -309,15 +310,11 @@ class RoomDetailFragment @Inject constructor(
displayRoomDetailActionResult(it)
}
roomDetailViewModel.viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
}
}
.disposeOnDestroyView()
roomDetailViewModel.observeViewEvents {
when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
}.exhaustive
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {

View File

@ -16,9 +16,11 @@
package im.vector.riotx.features.home.room.detail
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for RoomDetail
*/
sealed class RoomDetailViewEvents {
sealed class RoomDetailViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : RoomDetailViewEvents()
}

View File

@ -20,7 +20,12 @@ import android.net.Uri
import androidx.annotation.IdRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.*
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
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.jakewharton.rxrelay2.PublishRelay
import com.squareup.inject.assisted.Assisted
@ -55,9 +60,7 @@ 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.DataSource
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.core.utils.PublishDataSource
import im.vector.riotx.core.utils.subscribeLogError
import im.vector.riotx.features.command.CommandParser
import im.vector.riotx.features.command.ParsedCommand
@ -82,7 +85,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private val stringProvider: StringProvider,
private val typingHelper: TypingHelper,
private val session: Session
) : VectorViewModel<RoomDetailViewState, RoomDetailAction>(initialState), Timeline.Listener {
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener {
private val room = session.getRoom(initialState.roomId)!!
private val eventId = initialState.eventId
@ -105,9 +108,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
var timeline = room.createTimeline(eventId, timelineSettings)
private set
private val _viewEvents = PublishDataSource<RoomDetailViewEvents>()
val viewEvents: DataSource<RoomDetailViewEvents> = _viewEvents
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<RoomDetailAction>>>()
val requestLiveData: LiveData<LiveEvent<Async<RoomDetailAction>>>
@ -295,6 +295,7 @@ 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

View File

@ -32,6 +32,7 @@ import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
import im.vector.riotx.core.extensions.canReact
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.features.home.room.detail.timeline.format.NoticeEventFormatter
@ -86,7 +87,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private val noticeEventFormatter: NoticeEventFormatter,
private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences
) : VectorViewModel<MessageActionState, MessageActionsAction>(initialState) {
) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) {
private val eventId = initialState.eventId
private val informationData = initialState.informationData

View File

@ -15,7 +15,15 @@
*/
package im.vector.riotx.features.home.room.detail.timeline.edithistory
import com.airbnb.mvrx.*
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
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.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
@ -28,10 +36,11 @@ import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import timber.log.Timber
import java.util.*
import java.util.UUID
data class ViewEditHistoryViewState(
val eventId: String,
@ -47,7 +56,7 @@ class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
initialState: ViewEditHistoryViewState,
val session: Session,
val dateFormatter: VectorDateFormatter
) : VectorViewModel<ViewEditHistoryViewState, EmptyAction>(initialState) {
) : VectorViewModel<ViewEditHistoryViewState, EmptyAction, EmptyViewEvents>(initialState) {
private val roomId = initialState.roomId
private val eventId = initialState.eventId

View File

@ -16,7 +16,12 @@
package im.vector.riotx.features.home.room.detail.timeline.reactions
import com.airbnb.mvrx.*
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
@ -25,6 +30,7 @@ import im.vector.matrix.rx.RxRoom
import im.vector.matrix.rx.unwrap
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import io.reactivex.Observable
@ -54,7 +60,7 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted
initialState: DisplayReactionsViewState,
private val session: Session,
private val dateFormatter: VectorDateFormatter
) : VectorViewModel<DisplayReactionsViewState, EmptyAction>(initialState) {
) : VectorViewModel<DisplayReactionsViewState, EmptyAction, EmptyViewEvents>(initialState) {
private val roomId = initialState.roomId
private val eventId = initialState.eventId

View File

@ -0,0 +1,44 @@
/*
* Copyright 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.home.room.list
import im.vector.matrix.android.api.session.room.model.RoomSummary
import javax.inject.Inject
class BreadcrumbsRoomComparator @Inject constructor(
private val chronologicalRoomComparator: ChronologicalRoomComparator
) : Comparator<RoomSummary> {
override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
val leftBreadcrumbsIndex = leftRoomSummary?.breadcrumbsIndex ?: RoomSummary.NOT_IN_BREADCRUMBS
val rightBreadcrumbsIndex = rightRoomSummary?.breadcrumbsIndex ?: RoomSummary.NOT_IN_BREADCRUMBS
return if (leftBreadcrumbsIndex == RoomSummary.NOT_IN_BREADCRUMBS) {
if (rightBreadcrumbsIndex == RoomSummary.NOT_IN_BREADCRUMBS) {
chronologicalRoomComparator.compare(leftRoomSummary, rightRoomSummary)
} else {
1
}
} else {
if (rightBreadcrumbsIndex == RoomSummary.NOT_IN_BREADCRUMBS) {
-1
} else {
leftBreadcrumbsIndex - rightBreadcrumbsIndex
}
}
}
}

View File

@ -22,26 +22,20 @@ import javax.inject.Inject
class ChronologicalRoomComparator @Inject constructor() : Comparator<RoomSummary> {
override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
var rightTimestamp = 0L
var leftTimestamp = 0L
if (null != leftRoomSummary) {
leftTimestamp = leftRoomSummary.latestPreviewableEvent?.root?.originServerTs ?: 0
}
if (null != rightRoomSummary) {
rightTimestamp = rightRoomSummary.latestPreviewableEvent?.root?.originServerTs ?: 0
}
return if (rightRoomSummary?.latestPreviewableEvent?.root == null) {
-1
} else if (leftRoomSummary?.latestPreviewableEvent?.root == null) {
1
} else {
val deltaTimestamp = rightTimestamp - leftTimestamp
if (deltaTimestamp > 0) {
1
} else if (deltaTimestamp < 0) {
-1
} else {
0
return when {
rightRoomSummary?.latestPreviewableEvent?.root == null -> -1
leftRoomSummary?.latestPreviewableEvent?.root == null -> 1
else -> {
val rightTimestamp = rightRoomSummary.latestPreviewableEvent?.root?.originServerTs ?: 0
val leftTimestamp = leftRoomSummary.latestPreviewableEvent?.root?.originServerTs ?: 0
val deltaTimestamp = rightTimestamp - leftTimestamp
when {
deltaTimestamp > 0 -> 1
deltaTimestamp < 0 -> -1
else -> 0
}
}
}
}

View File

@ -27,7 +27,11 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.*
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
@ -35,6 +39,7 @@ import im.vector.matrix.android.api.session.room.notification.RoomNotificationSt
import im.vector.riotx.R
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.platform.OnBackPressed
import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment
@ -46,7 +51,6 @@ import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsShare
import im.vector.riotx.features.home.room.list.widget.FabMenuView
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.share.SharedData
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_list.*
import javax.inject.Inject
@ -98,16 +102,13 @@ class RoomListFragment @Inject constructor(
setupRecyclerView()
sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
roomListViewModel.subscribe { renderState(it) }
roomListViewModel.viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
when (it) {
is RoomListViewEvents.SelectRoom -> openSelectedRoom(it)
is RoomListViewEvents.Failure -> showErrorInSnackbar(it.throwable)
}
}
.disposeOnDestroyView()
roomListViewModel.observeViewEvents {
when (it) {
is RoomListViewEvents.Loading -> showLoading(it.message)
is RoomListViewEvents.Failure -> showFailure(it.throwable)
is RoomListViewEvents.SelectRoom -> openSelectedRoom(it)
}.exhaustive
}
createChatFabMenu.listener = this
@ -117,6 +118,10 @@ class RoomListFragment @Inject constructor(
.disposeOnDestroyView()
}
override fun showFailure(throwable: Throwable) {
showErrorInSnackbar(throwable)
}
override fun onDestroyView() {
roomController.removeModelBuildListener(modelBuildListener)
modelBuildListener = null

View File

@ -17,10 +17,14 @@
package im.vector.riotx.features.home.room.list
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for RoomList
*/
sealed class RoomListViewEvents {
sealed class RoomListViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : RoomListViewEvents()
data class Failure(val throwable: Throwable) : RoomListViewEvents()
data class SelectRoom(val roomId: String) : RoomListViewEvents()
}

View File

@ -26,7 +26,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.DataSource
import im.vector.riotx.core.utils.PublishDataSource
import im.vector.riotx.features.home.RoomListDisplayMode
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import javax.inject.Inject
@ -34,7 +34,7 @@ import javax.inject.Inject
class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
private val session: Session,
private val roomSummariesSource: DataSource<List<RoomSummary>>)
: VectorViewModel<RoomListViewState, RoomListAction>(initialState) {
: VectorViewModel<RoomListViewState, RoomListAction, RoomListViewEvents>(initialState) {
interface Factory {
fun create(initialState: RoomListViewState): RoomListViewModel
@ -52,9 +52,6 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
private val displayMode = initialState.displayMode
private val roomListDisplayModeFilter = RoomListDisplayModeFilter(displayMode)
private val _viewEvents = PublishDataSource<RoomListViewEvents>()
val viewEvents: DataSource<RoomListViewEvents> = _viewEvents
init {
observeRoomSummaries()
}
@ -197,6 +194,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
}
private fun handleLeaveRoom(action: RoomListAction.LeaveRoom) {
_viewEvents.post(RoomListViewEvents.Loading(null))
session.getRoom(action.roomId)?.leave(null, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
_viewEvents.post(RoomListViewEvents.Failure(failure))
@ -205,35 +203,54 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
}
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
// Set up init size on directChats and groupRooms as they are the biggest ones
val invites = ArrayList<RoomSummary>()
val favourites = ArrayList<RoomSummary>()
val directChats = ArrayList<RoomSummary>(rooms.size)
val groupRooms = ArrayList<RoomSummary>(rooms.size)
val lowPriorities = ArrayList<RoomSummary>()
val serverNotices = ArrayList<RoomSummary>()
if (displayMode == RoomListDisplayMode.SHARE) {
val recentRooms = ArrayList<RoomSummary>(20)
val otherRooms = ArrayList<RoomSummary>(rooms.size)
rooms
.filter { roomListDisplayModeFilter.test(it) }
.forEach { room ->
val tags = room.tags.map { it.name }
when {
room.membership == Membership.INVITE -> invites.add(room)
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
room.isDirect -> directChats.add(room)
else -> groupRooms.add(room)
rooms
.filter { roomListDisplayModeFilter.test(it) }
.forEach { room ->
when (room.breadcrumbsIndex) {
RoomSummary.NOT_IN_BREADCRUMBS -> otherRooms.add(room)
else -> recentRooms.add(room)
}
}
}
return RoomSummaries().apply {
put(RoomCategory.INVITE, invites)
put(RoomCategory.FAVOURITE, favourites)
put(RoomCategory.DIRECT, directChats)
put(RoomCategory.GROUP, groupRooms)
put(RoomCategory.LOW_PRIORITY, lowPriorities)
put(RoomCategory.SERVER_NOTICE, serverNotices)
return RoomSummaries().apply {
put(RoomCategory.RECENT_ROOMS, recentRooms)
put(RoomCategory.OTHER_ROOMS, otherRooms)
}
} else {
// Set up init size on directChats and groupRooms as they are the biggest ones
val invites = ArrayList<RoomSummary>()
val favourites = ArrayList<RoomSummary>()
val directChats = ArrayList<RoomSummary>(rooms.size)
val groupRooms = ArrayList<RoomSummary>(rooms.size)
val lowPriorities = ArrayList<RoomSummary>()
val serverNotices = ArrayList<RoomSummary>()
rooms
.filter { roomListDisplayModeFilter.test(it) }
.forEach { room ->
val tags = room.tags.map { it.name }
when {
room.membership == Membership.INVITE -> invites.add(room)
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
room.isDirect -> directChats.add(room)
else -> groupRooms.add(room)
}
}
return RoomSummaries().apply {
put(RoomCategory.INVITE, invites)
put(RoomCategory.FAVOURITE, favourites)
put(RoomCategory.DIRECT, directChats)
put(RoomCategory.GROUP, groupRooms)
put(RoomCategory.LOW_PRIORITY, lowPriorities)
put(RoomCategory.SERVER_NOTICE, serverNotices)
}
}
}
}

View File

@ -43,7 +43,10 @@ data class RoomListViewState(
val isDirectRoomsExpanded: Boolean = true,
val isGroupRoomsExpanded: Boolean = true,
val isLowPriorityRoomsExpanded: Boolean = true,
val isServerNoticeRoomsExpanded: Boolean = true
val isServerNoticeRoomsExpanded: Boolean = true,
// For sharing
val isRecentExpanded: Boolean = true,
val isOtherExpanded: Boolean = true
) : MvRxState {
constructor(args: RoomListParams) : this(displayMode = args.displayMode)
@ -56,6 +59,8 @@ data class RoomListViewState(
RoomCategory.GROUP -> isGroupRoomsExpanded
RoomCategory.LOW_PRIORITY -> isLowPriorityRoomsExpanded
RoomCategory.SERVER_NOTICE -> isServerNoticeRoomsExpanded
RoomCategory.RECENT_ROOMS -> isRecentExpanded
RoomCategory.OTHER_ROOMS -> isOtherExpanded
}
}
@ -67,6 +72,8 @@ data class RoomListViewState(
RoomCategory.GROUP -> copy(isGroupRoomsExpanded = !isGroupRoomsExpanded)
RoomCategory.LOW_PRIORITY -> copy(isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded)
RoomCategory.SERVER_NOTICE -> copy(isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded)
RoomCategory.RECENT_ROOMS -> copy(isRecentExpanded = !isRecentExpanded)
RoomCategory.OTHER_ROOMS -> copy(isOtherExpanded = !isOtherExpanded)
}
}
@ -86,7 +93,11 @@ enum class RoomCategory(@StringRes val titleRes: Int) {
DIRECT(R.string.bottom_action_people_x),
GROUP(R.string.bottom_action_rooms),
LOW_PRIORITY(R.string.low_priority_header),
SERVER_NOTICE(R.string.system_alerts_header)
SERVER_NOTICE(R.string.system_alerts_header),
// For Sharing
RECENT_ROOMS(R.string.room_list_sharing_header_recent_rooms),
OTHER_ROOMS(R.string.room_list_sharing_header_other_rooms)
}
fun RoomSummaries?.isNullOrEmpty(): Boolean {

View File

@ -59,39 +59,9 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
override fun buildModels() {
val nonNullViewState = viewState ?: return
when (nonNullViewState.displayMode) {
RoomListDisplayMode.FILTERED,
RoomListDisplayMode.SHARE -> {
buildFilteredRooms(nonNullViewState)
}
else -> {
var showHelp = false
val roomSummaries = nonNullViewState.asyncFilteredRooms()
roomSummaries?.forEach { (category, summaries) ->
if (summaries.isEmpty()) {
return@forEach
} else {
val isExpanded = nonNullViewState.isCategoryExpanded(category)
buildRoomCategory(nonNullViewState, summaries, category.titleRes, nonNullViewState.isCategoryExpanded(category)) {
listener?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(summaries,
nonNullViewState.joiningRoomsIds,
nonNullViewState.joiningErrorRoomsIds,
nonNullViewState.rejectingRoomsIds,
nonNullViewState.rejectingErrorRoomsIds)
// Never set showHelp to true for invitation
if (category != RoomCategory.INVITE) {
showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp()
}
}
}
}
if (showHelp) {
buildLongClickHelp()
}
}
RoomListDisplayMode.FILTERED -> buildFilteredRooms(nonNullViewState)
RoomListDisplayMode.SHARE -> buildShareRooms(nonNullViewState)
else -> buildRooms(nonNullViewState)
}
}
@ -109,9 +79,69 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds)
when {
viewState.displayMode == RoomListDisplayMode.FILTERED -> addFilterFooter(viewState)
filteredSummaries.isEmpty() -> addEmptyFooter()
addFilterFooter(viewState)
}
private fun buildShareRooms(viewState: RoomListViewState) {
var hasResult = false
val roomSummaries = viewState.asyncFilteredRooms()
roomListNameFilter.filter = viewState.roomFilter
roomSummaries?.forEach { (category, summaries) ->
val filteredSummaries = summaries
.filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
if (filteredSummaries.isEmpty()) {
return@forEach
} else {
hasResult = true
val isExpanded = viewState.isCategoryExpanded(category)
buildRoomCategory(viewState, emptyList(), category.titleRes, viewState.isCategoryExpanded(category)) {
listener?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(filteredSummaries,
emptySet(),
emptySet(),
emptySet(),
emptySet()
)
}
}
}
if (!hasResult) {
addNoResultItem()
}
}
private fun buildRooms(viewState: RoomListViewState) {
var showHelp = false
val roomSummaries = viewState.asyncFilteredRooms()
roomSummaries?.forEach { (category, summaries) ->
if (summaries.isEmpty()) {
return@forEach
} else {
val isExpanded = viewState.isCategoryExpanded(category)
buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) {
listener?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(summaries,
viewState.joiningRoomsIds,
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds)
// Never set showHelp to true for invitation
if (category != RoomCategory.INVITE) {
showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp()
}
}
}
}
if (showHelp) {
buildLongClickHelp()
}
}
@ -130,7 +160,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
}
}
private fun addEmptyFooter() {
private fun addNoResultItem() {
noResultItem {
id("no_result")
text(stringProvider.getString(R.string.no_result_placeholder))
@ -142,9 +172,6 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
@StringRes titleRes: Int,
isExpanded: Boolean,
mutateExpandedState: () -> Unit) {
if (summaries.isEmpty()) {
return
}
// TODO should add some business logic later
val unreadCount = if (summaries.isEmpty()) {
0

View File

@ -24,11 +24,12 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
class RoomListQuickActionsViewModel @AssistedInject constructor(@Assisted initialState: RoomListQuickActionsState,
session: Session
) : VectorViewModel<RoomListQuickActionsState, EmptyAction>(initialState) {
) : VectorViewModel<RoomListQuickActionsState, EmptyAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {

View File

@ -27,9 +27,9 @@ import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.riotx.R
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.OnBackPressed
import im.vector.riotx.core.platform.VectorBaseFragment
import io.reactivex.android.schedulers.AndroidSchedulers
import javax.net.ssl.HttpsURLConnection
/**
@ -59,25 +59,21 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java)
loginViewModel.viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
handleLoginViewEvents(it)
}
.disposeOnDestroyView()
loginViewModel.observeViewEvents {
handleLoginViewEvents(it)
}
}
private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) {
when (loginViewEvents) {
is LoginViewEvents.Error -> showError(loginViewEvents.throwable)
else ->
is LoginViewEvents.Failure -> showFailure(loginViewEvents.throwable)
else ->
// This is handled by the Activity
Unit
}
}.exhaustive
}
private fun showError(throwable: Throwable) {
override fun showFailure(throwable: Throwable) {
when (throwable) {
is Failure.ServerError -> {
if (throwable.error.code == MatrixError.M_FORBIDDEN
@ -96,11 +92,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
}
open fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
super.showFailure(throwable)
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {

View File

@ -209,7 +209,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
.setMessage(R.string.login_error_outdated_homeserver_content)
.setPositiveButton(R.string.ok, null)
.show()
is LoginViewEvents.Error ->
is LoginViewEvents.Failure ->
// This is handled by the Fragments
Unit
}

View File

@ -18,12 +18,15 @@
package im.vector.riotx.features.login
import im.vector.matrix.android.api.auth.registration.FlowResult
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for Login
*/
sealed class LoginViewEvents {
sealed class LoginViewEvents: VectorViewEvents {
data class Loading(val message: CharSequence? = null) : LoginViewEvents()
data class Failure(val throwable: Throwable) : LoginViewEvents()
data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : LoginViewEvents()
data class Error(val throwable: Throwable) : LoginViewEvents()
object OutdatedHomeserver : LoginViewEvents()
}

View File

@ -41,8 +41,6 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.configureAndStart
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.DataSource
import im.vector.riotx.core.utils.PublishDataSource
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.session.SessionListener
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
@ -59,7 +57,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
private val pushRuleTriggerListener: PushRuleTriggerListener,
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
private val sessionListener: SessionListener)
: VectorViewModel<LoginViewState, LoginAction>(initialState) {
: VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -95,9 +93,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
private var currentTask: Cancelable? = null
private val _viewEvents = PublishDataSource<LoginViewEvents>()
val viewEvents: DataSource<LoginViewEvents> = _viewEvents
override fun handle(action: LoginAction) {
when (action) {
is LoginAction.UpdateServerType -> handleUpdateServerType(action)
@ -179,7 +174,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
override fun onFailure(failure: Throwable) {
if (failure !is CancellationException) {
_viewEvents.post(LoginViewEvents.Error(failure))
_viewEvents.post(LoginViewEvents.Failure(failure))
}
setState {
copy(
@ -201,7 +196,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(LoginViewEvents.Error(failure))
_viewEvents.post(LoginViewEvents.Failure(failure))
setState {
copy(
asyncRegistration = Uninitialized
@ -223,7 +218,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(LoginViewEvents.Error(failure))
_viewEvents.post(LoginViewEvents.Failure(failure))
setState {
copy(
asyncRegistration = Uninitialized
@ -526,7 +521,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
if (homeServerConnectionConfig == null) {
// This is invalid
_viewEvents.post(LoginViewEvents.Error(Throwable("Unable to create a HomeServerConnectionConfig")))
_viewEvents.post(LoginViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
} else {
currentTask?.cancel()
currentTask = null
@ -540,7 +535,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
currentTask = authenticationService.getLoginFlow(homeServerConnectionConfig, object : MatrixCallback<LoginFlowResult> {
override fun onFailure(failure: Throwable) {
_viewEvents.post(LoginViewEvents.Error(failure))
_viewEvents.post(LoginViewEvents.Failure(failure))
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized

View File

@ -21,6 +21,7 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.reactions.data.EmojiDataSource
import im.vector.riotx.features.reactions.data.EmojiItem
@ -33,7 +34,7 @@ data class EmojiSearchResultViewState(
class EmojiSearchResultViewModel @AssistedInject constructor(
@Assisted initialState: EmojiSearchResultViewState,
private val dataSource: EmojiDataSource)
: VectorViewModel<EmojiSearchResultViewState, EmojiSearchAction>(initialState) {
: VectorViewModel<EmojiSearchResultViewState, EmojiSearchAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {

View File

@ -75,6 +75,7 @@ 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()

View File

@ -33,6 +33,7 @@ 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
@ -41,7 +42,7 @@ private const val PUBLIC_ROOMS_LIMIT = 20
class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: PublicRoomsViewState,
private val session: Session)
: VectorViewModel<PublicRoomsViewState, RoomDirectoryAction>(initialState) {
: VectorViewModel<PublicRoomsViewState, RoomDirectoryAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {

View File

@ -17,7 +17,12 @@
package im.vector.riotx.features.roomdirectory.createroom
import androidx.fragment.app.FragmentActivity
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.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
@ -25,12 +30,13 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateRoomViewState,
private val session: Session
) : VectorViewModel<CreateRoomViewState, CreateRoomAction>(initialState) {
) : VectorViewModel<CreateRoomViewState, CreateRoomAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {

View File

@ -16,17 +16,22 @@
package im.vector.riotx.features.roomdirectory.picker
import com.airbnb.mvrx.*
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
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
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initialState: RoomDirectoryPickerViewState,
private val session: Session)
: VectorViewModel<RoomDirectoryPickerViewState, RoomDirectoryPickerAction>(initialState) {
: VectorViewModel<RoomDirectoryPickerViewState, RoomDirectoryPickerAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {

View File

@ -26,13 +26,14 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.roomdirectory.JoinState
import timber.log.Timber
class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: RoomPreviewViewState,
private val session: Session)
: VectorViewModel<RoomPreviewViewState, RoomPreviewAction>(initialState) {
: VectorViewModel<RoomPreviewViewState, RoomPreviewAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {

View File

@ -20,13 +20,19 @@ package im.vector.riotx.features.roommemberprofile
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import com.airbnb.mvrx.*
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.animations.AppBarStateChangeListener
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.setTextOrHide
import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment
@ -73,16 +79,13 @@ class RoomMemberProfileFragment @Inject constructor(
appBarStateChangeListener = MatrixItemAppBarStateChangeListener(headerView, listOf(matrixProfileToolbarAvatarImageView,
matrixProfileToolbarTitleView))
matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener)
viewModel.viewEvents
.observe()
.subscribe {
dismissLoadingDialog()
when (it) {
is RoomMemberProfileViewEvents.Loading -> showLoadingDialog(it.message)
is RoomMemberProfileViewEvents.Failure -> showErrorInSnackbar(it.throwable)
}
}
.disposeOnDestroyView()
viewModel.observeViewEvents {
when (it) {
is RoomMemberProfileViewEvents.Loading -> showLoading(it.message)
is RoomMemberProfileViewEvents.Failure -> showFailure(it.throwable)
is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit
}.exhaustive
}
}
override fun onDestroyView() {

View File

@ -16,11 +16,14 @@
package im.vector.riotx.features.roommemberprofile
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for RoomMemberProfile
*/
sealed class RoomMemberProfileViewEvents {
data class Loading(val message: CharSequence) : RoomMemberProfileViewEvents()
object OnIgnoreActionSuccess : RoomMemberProfileViewEvents()
sealed class RoomMemberProfileViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : RoomMemberProfileViewEvents()
data class Failure(val throwable: Throwable) : RoomMemberProfileViewEvents()
object OnIgnoreActionSuccess : RoomMemberProfileViewEvents()
}

View File

@ -44,8 +44,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.DataSource
import im.vector.riotx.core.utils.PublishDataSource
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import kotlinx.coroutines.Dispatchers
@ -55,7 +53,7 @@ import kotlinx.coroutines.withContext
class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private val initialState: RoomMemberProfileViewState,
private val stringProvider: StringProvider,
private val session: Session)
: VectorViewModel<RoomMemberProfileViewState, RoomMemberProfileAction>(initialState) {
: VectorViewModel<RoomMemberProfileViewState, RoomMemberProfileAction, RoomMemberProfileViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -71,9 +69,6 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
}
}
private val _viewEvents = PublishDataSource<RoomMemberProfileViewEvents>()
val viewEvents: DataSource<RoomMemberProfileViewEvents> = _viewEvents
private val room = if (initialState.roomId != null) {
session.getRoom(initialState.roomId)
} else {
@ -183,7 +178,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
private fun handleIgnoreAction() = withState { state ->
val isIgnored = state.isIgnored() ?: return@withState
_viewEvents.post(RoomMemberProfileViewEvents.Loading(stringProvider.getString(R.string.please_wait)))
_viewEvents.post(RoomMemberProfileViewEvents.Loading())
val ignoreActionCallback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_viewEvents.post(RoomMemberProfileViewEvents.OnIgnoreActionSuccess)

View File

@ -26,6 +26,7 @@ import im.vector.riotx.core.extensions.addFragmentToBackstack
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment
class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable {
@ -69,7 +70,7 @@ class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable {
}
private fun openRoomSettings() {
notImplemented("Open room settings")
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomSettingsFragment::class.java, roomProfileArgs)
}
private fun openRoomMembers() {

View File

@ -31,6 +31,7 @@ import im.vector.riotx.core.animations.AppBarStateChangeListener
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.setTextOrHide
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.AvatarRenderer
@ -77,17 +78,13 @@ class RoomProfileFragment @Inject constructor(
appBarStateChangeListener = MatrixItemAppBarStateChangeListener(headerView, listOf(matrixProfileToolbarAvatarImageView,
matrixProfileToolbarTitleView))
matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener)
roomProfileViewModel.viewEvents
.observe()
.subscribe {
dismissLoadingDialog()
when (it) {
is RoomProfileViewEvents.Loading -> showLoadingDialog(it.message)
RoomProfileViewEvents.OnLeaveRoomSuccess -> onLeaveRoom()
is RoomProfileViewEvents.Failure -> showError(it.throwable)
}
}
.disposeOnDestroyView()
roomProfileViewModel.observeViewEvents {
when (it) {
is RoomProfileViewEvents.Loading -> showLoading(it.message)
is RoomProfileViewEvents.Failure -> showFailure(it.throwable)
is RoomProfileViewEvents.OnLeaveRoomSuccess -> onLeaveRoom()
}.exhaustive
}
roomListQuickActionsSharedActionViewModel
.observe()
.subscribe { handleQuickActions(it) }

View File

@ -15,11 +15,15 @@
*/
package im.vector.riotx.features.roomprofile
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for RoomProfile
*/
sealed class RoomProfileViewEvents {
data class Loading(val message: CharSequence): RoomProfileViewEvents()
object OnLeaveRoomSuccess: RoomProfileViewEvents()
sealed class RoomProfileViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents()
data class Failure(val throwable: Throwable) : RoomProfileViewEvents()
object OnLeaveRoomSuccess : RoomProfileViewEvents()
}

View File

@ -29,13 +29,11 @@ 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.DataSource
import im.vector.riotx.core.utils.PublishDataSource
class RoomProfileViewModel @AssistedInject constructor(@Assisted initialState: RoomProfileViewState,
private val stringProvider: StringProvider,
private val session: Session)
: VectorViewModel<RoomProfileViewState, RoomProfileAction>(initialState) {
: VectorViewModel<RoomProfileViewState, RoomProfileAction, RoomProfileViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -51,9 +49,6 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted initialState: R
}
}
private val _viewEvents = PublishDataSource<RoomProfileViewEvents>()
val viewEvents: DataSource<RoomProfileViewEvents> = _viewEvents
private val room = session.getRoom(initialState.roomId)!!
init {

View File

@ -29,7 +29,7 @@ import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.roomprofile.RoomProfileArgs
import kotlinx.android.synthetic.main.fragment_room_member_list.*
import kotlinx.android.synthetic.main.fragment_room_setting_generic.*
import javax.inject.Inject
class RoomMemberListFragment @Inject constructor(
@ -41,12 +41,12 @@ class RoomMemberListFragment @Inject constructor(
private val viewModel: RoomMemberListViewModel by fragmentViewModel()
private val roomProfileArgs: RoomProfileArgs by args()
override fun getLayoutResId() = R.layout.fragment_room_member_list
override fun getLayoutResId() = R.layout.fragment_room_setting_generic
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
roomMemberListController.callback = this
setupToolbar(roomMemberListToolbar)
setupToolbar(roomSettingsToolbar)
recyclerView.configureWith(roomMemberListController, hasFixedSize = true)
}
@ -66,8 +66,8 @@ class RoomMemberListFragment @Inject constructor(
private fun renderRoomSummary(state: RoomMemberListViewState) {
state.roomSummary()?.let {
roomMemberListToolbarTitleView.text = it.displayName
avatarRenderer.render(it.toMatrixItem(), roomMemberListToolbarAvatarImageView)
roomSettingsToolbarTitleView.text = it.displayName
avatarRenderer.render(it.toMatrixItem(), roomSettingsToolbarAvatarImageView)
}
}
}

View File

@ -34,13 +34,14 @@ import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
import im.vector.matrix.rx.mapOptional
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState: RoomMemberListViewState,
private val session: Session)
: VectorViewModel<RoomMemberListViewState, RoomMemberListAction>(initialState) {
: VectorViewModel<RoomMemberListViewState, RoomMemberListAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {

View File

@ -0,0 +1,26 @@
/*
* Copyright 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.roomprofile.settings
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomSettingsAction : VectorViewModelAction {
data class SetRoomName(val newName: String) : RoomSettingsAction()
data class SetRoomTopic(val newTopic: String) : RoomSettingsAction()
data class SetRoomAvatar(val newAvatarUrl: String) : RoomSettingsAction()
object EnableEncryption : RoomSettingsAction()
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 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.roomprofile.settings
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.profiles.buildProfileAction
import im.vector.riotx.core.epoxy.profiles.buildProfileSection
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import javax.inject.Inject
// TODO Add other feature here (waiting for design)
class RoomSettingsController @Inject constructor(
private val stringProvider: StringProvider,
colorProvider: ColorProvider
) : TypedEpoxyController<RoomSettingsViewState>() {
interface Callback {
fun onEnableEncryptionClicked()
}
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
var callback: Callback? = null
init {
setData(null)
}
override fun buildModels(data: RoomSettingsViewState?) {
val roomSummary = data?.roomSummary?.invoke() ?: return
buildProfileSection(
stringProvider.getString(R.string.settings)
)
if (roomSummary.isEncrypted) {
buildProfileAction(
id = "encryption",
title = stringProvider.getString(R.string.room_settings_addresses_e2e_enabled),
dividerColor = dividerColor,
divider = false,
editable = false
)
} else {
buildProfileAction(
id = "encryption",
title = stringProvider.getString(R.string.room_settings_enable_encryption),
subtitle = stringProvider.getString(R.string.room_settings_enable_encryption_warning),
dividerColor = dividerColor,
divider = false,
editable = true,
action = { callback?.onEnableEncryptionClicked() }
)
}
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright 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.roomprofile.settings
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.util.toMatrixItem
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.exhaustive
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.roomprofile.RoomProfileArgs
import kotlinx.android.synthetic.main.fragment_room_setting_generic.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import javax.inject.Inject
class RoomSettingsFragment @Inject constructor(
val viewModelFactory: RoomSettingsViewModel.Factory,
private val controller: RoomSettingsController,
private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment(), RoomSettingsController.Callback {
private val viewModel: RoomSettingsViewModel by fragmentViewModel()
private val roomProfileArgs: RoomProfileArgs by args()
override fun getLayoutResId() = R.layout.fragment_room_setting_generic
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
controller.callback = this
setupToolbar(roomSettingsToolbar)
recyclerView.configureWith(controller, hasFixedSize = true)
waiting_view_status_text.setText(R.string.please_wait)
waiting_view_status_text.isVisible = true
viewModel.observeViewEvents {
when (it) {
is RoomSettingsViewEvents.Failure -> showFailure(it.throwable)
}.exhaustive
}
}
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { viewState ->
controller.setData(viewState)
renderRoomSummary(viewState)
}
override fun onEnableEncryptionClicked() {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.room_settings_enable_encryption_dialog_title)
.setMessage(R.string.room_settings_enable_encryption_dialog_content)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.room_settings_enable_encryption_dialog_submit) { _, _ ->
viewModel.handle(RoomSettingsAction.EnableEncryption)
}
.show()
}
private fun renderRoomSummary(state: RoomSettingsViewState) {
waiting_view.isVisible = state.isLoading
state.roomSummary()?.let {
roomSettingsToolbarTitleView.text = it.displayName
avatarRenderer.render(it.toMatrixItem(), roomSettingsToolbarAvatarImageView)
}
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 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.roomprofile.settings
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for room settings screen
*/
sealed class RoomSettingsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : RoomSettingsViewEvents()
}

View File

@ -0,0 +1,90 @@
/*
* Copyright 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.roomprofile.settings
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
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.session.Session
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.core.platform.VectorViewModel
class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState,
private val session: Session)
: VectorViewModel<RoomSettingsViewState, RoomSettingsAction, RoomSettingsViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: RoomSettingsViewState): RoomSettingsViewModel
}
companion object : MvRxViewModelFactory<RoomSettingsViewModel, RoomSettingsViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomSettingsViewState): RoomSettingsViewModel? {
val fragment: RoomSettingsFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewModelFactory.create(state)
}
}
private val room = session.getRoom(initialState.roomId)!!
init {
observeRoomSummary()
}
private fun observeRoomSummary() {
room.rx().liveRoomSummary()
.unwrap()
.execute { async ->
copy(roomSummary = async)
}
}
override fun handle(action: RoomSettingsAction) {
when (action) {
is RoomSettingsAction.EnableEncryption -> handleEnableEncryption()
}
}
private fun handleEnableEncryption() {
setState {
copy(isLoading = true)
}
room.enableEncryption(MXCRYPTO_ALGORITHM_MEGOLM, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
setState {
copy(isLoading = false)
}
_viewEvents.post(RoomSettingsViewEvents.Failure(failure))
}
override fun onSuccess(data: Unit) {
setState {
copy(isLoading = false)
}
}
})
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 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.roomprofile.settings
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.features.roomprofile.RoomProfileArgs
data class RoomSettingsViewState(
val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized,
val isLoading: Boolean = false
) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 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.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 deviceInfo: DeviceInfo) : DevicesAction()
data class Password(val password: String) : DevicesAction()
data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction()
data class ToggleDevice(val deviceInfo: DeviceInfo) : DevicesAction()
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 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.devices
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for Ignored users screen
*/
sealed class DevicesViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : DevicesViewEvents()
data class Failure(val throwable: Throwable) : DevicesViewEvents()
}

View File

@ -18,7 +18,15 @@ package im.vector.riotx.features.settings.devices
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.*
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
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.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
@ -29,7 +37,6 @@ import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.core.utils.LiveEvent
import timber.log.Timber
@ -37,20 +44,13 @@ data class DevicesViewState(
val myDeviceId: String = "",
val devices: Async<List<DeviceInfo>> = Uninitialized,
val currentExpandedDeviceId: String? = null,
// TODO Replace by isLoading boolean
val request: Async<Unit> = Uninitialized
) : MvRxState
sealed class DevicesAction : VectorViewModelAction {
object Retry : DevicesAction()
data class Delete(val deviceInfo: DeviceInfo) : DevicesAction()
data class Password(val password: String) : DevicesAction()
data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction()
data class ToggleDevice(val deviceInfo: DeviceInfo) : DevicesAction()
}
class DevicesViewModel @AssistedInject constructor(@Assisted initialState: DevicesViewState,
private val session: Session)
: VectorViewModel<DevicesViewState, DevicesAction>(initialState) {
: VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -153,7 +153,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
)
}
_requestErrorLiveData.postLiveEvent(failure)
_viewEvents.post(DevicesViewEvents.Failure(failure))
}
})
}
@ -207,7 +207,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
)
}
_requestErrorLiveData.postLiveEvent(failure)
_viewEvents.post(DevicesViewEvents.Failure(failure))
}
}
@ -261,7 +261,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
)
}
_requestErrorLiveData.postLiveEvent(failure)
_viewEvents.post(DevicesViewEvents.Failure(failure))
}
})
}

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
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.exhaustive
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
@ -52,7 +53,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
override fun getLayoutResId() = R.layout.fragment_generic_recycler
private val devicesViewModel: DevicesViewModel by fragmentViewModel()
private val viewModel: DevicesViewModel by fragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -61,16 +62,24 @@ class VectorSettingsDevicesFragment @Inject constructor(
waiting_view_status_text.isVisible = true
devicesController.callback = this
recyclerView.configureWith(devicesController, showDivider = true)
devicesViewModel.requestErrorLiveData.observeEvent(this) {
displayErrorDialog(it)
// Password is maybe not good, for safety measure, reset it here
mAccountPassword = ""
viewModel.observeViewEvents {
when (it) {
is DevicesViewEvents.Loading -> showLoading(it.message)
is DevicesViewEvents.Failure -> showFailure(it.throwable)
}.exhaustive
}
devicesViewModel.requestPasswordLiveData.observeEvent(this) {
viewModel.requestPasswordLiveData.observeEvent(this) {
maybeShowDeleteDeviceWithPasswordDialog()
}
}
override fun showFailure(throwable: Throwable) {
super.showFailure(throwable)
// Password is maybe not good, for safety measure, reset it here
mAccountPassword = ""
}
override fun onDestroyView() {
devicesController.callback = null
recyclerView.cleanup()
@ -83,20 +92,12 @@ class VectorSettingsDevicesFragment @Inject constructor(
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_devices_list)
}
private fun displayErrorDialog(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun onDeviceClicked(deviceInfo: DeviceInfo) {
devicesViewModel.handle(DevicesAction.ToggleDevice(deviceInfo))
viewModel.handle(DevicesAction.ToggleDevice(deviceInfo))
}
override fun onDeleteDevice(deviceInfo: DeviceInfo) {
devicesViewModel.handle(DevicesAction.Delete(deviceInfo))
viewModel.handle(DevicesAction.Delete(deviceInfo))
}
override fun onRenameDevice(deviceInfo: DeviceInfo) {
@ -104,7 +105,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
}
override fun retry() {
devicesViewModel.handle(DevicesAction.Retry)
viewModel.handle(DevicesAction.Retry)
}
/**
@ -125,7 +126,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
.setPositiveButton(R.string.ok) { _, _ ->
val newName = input.text.toString()
devicesViewModel.handle(DevicesAction.Rename(deviceInfo, newName))
viewModel.handle(DevicesAction.Rename(deviceInfo, newName))
}
.setNegativeButton(R.string.cancel, null)
.show()
@ -136,7 +137,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
*/
private fun maybeShowDeleteDeviceWithPasswordDialog() {
if (mAccountPassword.isNotEmpty()) {
devicesViewModel.handle(DevicesAction.Password(mAccountPassword))
viewModel.handle(DevicesAction.Password(mAccountPassword))
} else {
val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_device_delete, null)
@ -152,7 +153,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
return@OnClickListener
}
mAccountPassword = passwordEditText.text.toString()
devicesViewModel.handle(DevicesAction.Password(mAccountPassword))
viewModel.handle(DevicesAction.Password(mAccountPassword))
})
.setNegativeButton(R.string.cancel, null)
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
@ -166,7 +167,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
}
}
override fun invalidate() = withState(devicesViewModel) { state ->
override fun invalidate() = withState(viewModel) { state ->
devicesController.update(state)
handleRequestStatus(state.request)

View File

@ -0,0 +1,28 @@
/*
* 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.settings.ignored
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for Ignored users screen
*/
sealed class IgnoredUsersViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : IgnoredUsersViewEvents()
data class Failure(val throwable: Throwable) : IgnoredUsersViewEvents()
}

View File

@ -16,14 +16,21 @@
package im.vector.riotx.features.settings.ignored
import com.airbnb.mvrx.*
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
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.Uninitialized
import com.airbnb.mvrx.ViewModelContext
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.session.Session
import im.vector.matrix.android.api.session.user.model.User
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.platform.VectorViewModelAction
@ -38,7 +45,7 @@ sealed class IgnoredUsersAction : VectorViewModelAction {
class IgnoredUsersViewModel @AssistedInject constructor(@Assisted initialState: IgnoredUsersViewState,
private val session: Session)
: VectorViewModel<IgnoredUsersViewState, IgnoredUsersAction>(initialState) {
: VectorViewModel<IgnoredUsersViewState, IgnoredUsersAction, IgnoredUsersViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -89,7 +96,7 @@ class IgnoredUsersViewModel @AssistedInject constructor(@Assisted initialState:
)
}
_requestErrorLiveData.postLiveEvent(failure)
_viewEvents.post(IgnoredUsersViewEvents.Failure(failure))
}
override fun onSuccess(data: Unit) {

View File

@ -27,7 +27,7 @@ import com.airbnb.mvrx.withState
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.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
@ -41,7 +41,7 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor(
override fun getLayoutResId() = R.layout.fragment_generic_recycler
private val ignoredUsersViewModel: IgnoredUsersViewModel by fragmentViewModel()
private val viewModel: IgnoredUsersViewModel by fragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -50,8 +50,11 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor(
waiting_view_status_text.isVisible = true
ignoredUsersController.callback = this
recyclerView.configureWith(ignoredUsersController)
ignoredUsersViewModel.requestErrorLiveData.observeEvent(this) {
displayErrorDialog(it)
viewModel.observeViewEvents {
when (it) {
is IgnoredUsersViewEvents.Loading -> showLoading(it.message)
is IgnoredUsersViewEvents.Failure -> showFailure(it.throwable)
}.exhaustive
}
}
@ -71,25 +74,17 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor(
AlertDialog.Builder(requireActivity())
.setMessage(getString(R.string.settings_unignore_user, userId))
.setPositiveButton(R.string.yes) { _, _ ->
ignoredUsersViewModel.handle(IgnoredUsersAction.UnIgnore(userId))
viewModel.handle(IgnoredUsersAction.UnIgnore(userId))
}
.setNegativeButton(R.string.no, null)
.show()
}
private fun displayErrorDialog(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
// ==============================================================================================================
// ignored users list management
// ==============================================================================================================
override fun invalidate() = withState(ignoredUsersViewModel) { state ->
override fun invalidate() = withState(viewModel) { state ->
ignoredUsersController.update(state)
handleUnIgnoreRequestStatus(state.unIgnoreRequest)

View File

@ -16,13 +16,19 @@
package im.vector.riotx.features.settings.push
import com.airbnb.mvrx.*
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.rx.RxSession
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
data class PushGatewayViewState(
@ -31,7 +37,7 @@ data class PushGatewayViewState(
class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: PushGatewayViewState,
private val session: Session)
: VectorViewModel<PushGatewayViewState, EmptyAction>(initialState) {
: VectorViewModel<PushGatewayViewState, EmptyAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {

View File

@ -21,6 +21,7 @@ import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.pushrules.rest.PushRule
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.platform.VectorViewModel
data class PushRulesViewState(
@ -28,7 +29,7 @@ data class PushRulesViewState(
) : MvRxState
class PushRulesViewModel(initialState: PushRulesViewState)
: VectorViewModel<PushRulesViewState, EmptyAction>(initialState) {
: VectorViewModel<PushRulesViewState, EmptyAction, EmptyViewEvents>(initialState) {
companion object : MvRxViewModelFactory<PushRulesViewModel, PushRulesViewState> {

View File

@ -44,7 +44,9 @@ class IncomingShareActivity :
@Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var incomingShareViewModelFactory: IncomingShareViewModel.Factory
private lateinit var attachmentsHelper: AttachmentsHelper
private val incomingShareViewModel: IncomingShareViewModel by viewModel()
// Do not remove, even if not used, it instantiates the view model
@Suppress("unused")
private val viewModel: IncomingShareViewModel by viewModel()
private val roomListFragment: RoomListFragment?
get() {
return supportFragmentManager.findFragmentById(R.id.shareRoomListFragmentContainer) as? RoomListFragment

View File

@ -26,7 +26,9 @@ import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.rx.rx
import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.list.BreadcrumbsRoomComparator
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
@ -38,8 +40,9 @@ data class IncomingShareState(private val dummy: Boolean = false) : MvRxState
*/
class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: IncomingShareState,
private val sessionObservableStore: ActiveSessionDataSource,
private val shareRoomListObservableStore: ShareRoomListDataSource)
: VectorViewModel<IncomingShareState, EmptyAction>(initialState) {
private val shareRoomListObservableStore: ShareRoomListDataSource,
private val breadcrumbsRoomComparator: BreadcrumbsRoomComparator)
: VectorViewModel<IncomingShareState, EmptyAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -68,6 +71,9 @@ class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState:
?: Observable.just(emptyList())
}
.throttleLast(300, TimeUnit.MILLISECONDS)
.map {
it.sortedWith(breadcrumbsRoomComparator)
}
.subscribe {
shareRoomListObservableStore.post(it)
}

View File

@ -73,7 +73,7 @@ class SoftLogoutActivity : LoginActivity() {
private fun handleSoftLogoutViewEvents(softLogoutViewEvents: SoftLogoutViewEvents) {
when (softLogoutViewEvents) {
is SoftLogoutViewEvents.Error ->
is SoftLogoutViewEvents.Failure ->
showError(errorFormatter.toHumanReadable(softLogoutViewEvents.throwable))
is SoftLogoutViewEvents.ErrorNotSameUser -> {
// Pop the backstack

View File

@ -17,11 +17,14 @@
package im.vector.riotx.features.signout.soft
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for SoftLogout
*/
sealed class SoftLogoutViewEvents {
sealed class SoftLogoutViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : SoftLogoutViewEvents()
data class ErrorNotSameUser(val currentUserId: String, val newUserId: String) : SoftLogoutViewEvents()
data class Error(val throwable: Throwable) : SoftLogoutViewEvents()
object ClearData : SoftLogoutViewEvents()
}

View File

@ -16,7 +16,13 @@
package im.vector.riotx.features.signout.soft
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.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
@ -28,8 +34,6 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.hasUnsavedKeys
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.DataSource
import im.vector.riotx.core.utils.PublishDataSource
import im.vector.riotx.features.login.LoginMode
import timber.log.Timber
@ -41,7 +45,7 @@ class SoftLogoutViewModel @AssistedInject constructor(
private val session: Session,
private val activeSessionHolder: ActiveSessionHolder,
private val authenticationService: AuthenticationService
) : VectorViewModel<SoftLogoutViewState, SoftLogoutAction>(initialState) {
) : VectorViewModel<SoftLogoutViewState, SoftLogoutAction, SoftLogoutViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -71,9 +75,6 @@ class SoftLogoutViewModel @AssistedInject constructor(
private var currentTask: Cancelable? = null
private val _viewEvents = PublishDataSource<SoftLogoutViewEvents>()
val viewEvents: DataSource<SoftLogoutViewEvents> = _viewEvents
init {
// Get the supported login flow
getSupportedLoginFlow()
@ -192,7 +193,7 @@ class SoftLogoutViewModel @AssistedInject constructor(
currentTask = session.updateCredentials(action.credentials,
object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
_viewEvents.post(SoftLogoutViewEvents.Error(failure))
_viewEvents.post(SoftLogoutViewEvents.Failure(failure))
setState {
copy(
asyncLoginAction = Uninitialized

View File

@ -4,25 +4,26 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rootConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="?riotx_header_panel_background">
<androidx.appcompat.widget.Toolbar
android:id="@+id/roomMemberListToolbar"
android:id="@+id/roomSettingsToolbar"
style="@style/VectorToolbarStyle"
android:elevation="4dp"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
android:elevation="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/roomMemberListToolbarContentView"
android:id="@+id/roomSettingsToolbarContentView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/roomMemberListToolbarAvatarImageView"
android:id="@+id/roomSettingsToolbarAvatarImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
@ -33,7 +34,7 @@
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/roomMemberListToolbarTitleView"
android:id="@+id/roomSettingsToolbarTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
@ -42,9 +43,9 @@
android:maxLines="1"
android:textColor="?vctr_toolbar_primary_text_color"
android:textSize="18sp"
app:layout_constraintStart_toEndOf="@+id/roomMemberListToolbarAvatarImageView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/roomSettingsToolbarAvatarImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/roomName" />
@ -57,10 +58,12 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:overScrollMode="always"
app:layout_constraintTop_toBottomOf="@+id/roomMemberListToolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:listitem="@layout/item_autocomplete_matrix_item" />
app:layout_constraintTop_toBottomOf="@+id/roomSettingsToolbar"
tools:listitem="@layout/item_profile_action" />
<include layout="@layout/merge_overlay_waiting_view" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -10,6 +10,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?vctr_waiting_background_color"
android:clickable="true"
android:focusable="true"
android:visibility="gone"
tools:visibility="visible">

View File

@ -80,12 +80,21 @@
<string name="unignore">Unignore</string>
<string name="room_list_sharing_header_recent_rooms">Recent rooms</string>
<string name="room_list_sharing_header_other_rooms">Other rooms</string>
<!-- Title for category in the settings which affect what is displayed in the timeline (ex: show read receipts, etc.) -->
<string name="settings_category_timeline">Timeline</string>
<!-- Title for category in the settings which affect the behavior of the message editor (ex: enable Markdown, send typing notification, etc.) -->
<string name="settings_category_composer">Message editor</string>
<string name="room_settings_enable_encryption">Enable end-to-end encryption</string>
<string name="room_settings_enable_encryption_warning">Once enabled, encryption cannot be disabled.</string>
<string name="room_settings_enable_encryption_dialog_title">Enable encryption?</string>
<string name="room_settings_enable_encryption_dialog_content">Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly.</string>
<string name="room_settings_enable_encryption_dialog_submit">Enable encryption</string>
<string name="verification_request_notice">For extra security, verify %s by checking a one-time code.</string>