Merge branch 'develop' into feature/stabilization_2

This commit is contained in:
Ganard 2020-01-31 15:47:33 +01:00
commit ec6d78bf96
108 changed files with 1794 additions and 458 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,13 +2,16 @@ Changes in RiotX 0.14.0 (2020-XX-XX)
===================================================
Features ✨:
-
- Enable encryption in unencrypted rooms, from the room settings (#212)
- Enable e2e by default when creating DM, and give the possibility to enable encryption when creating room (#837)
Improvements 🙌:
-
- Sharing things to RiotX: sort list by recent room first (#771)
- Hide the algorithm when turning on e2e (#897)
- Sort room members by display names
Other changes:
-
- Add support for /rainbow and /rainbowme commands (#879)
Bugfix 🐛:
-
@ -17,7 +20,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

@ -12,12 +12,13 @@ RiotX is an Android Matrix Client currently in beta but in active development.
It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience. RiotX will become the official replacement as soon as all features are implemented.
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx)
[<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.riotx)
Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
# 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

@ -56,9 +56,15 @@ import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.*
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceTask
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.database.query.whereType
import im.vector.matrix.android.internal.di.MoshiProvider
@ -72,7 +78,12 @@ import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.fetchCopied
import kotlinx.coroutines.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.matrix.olm.OlmManager
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
@ -476,14 +487,16 @@ internal class DefaultCryptoService @Inject constructor(
}
/**
* Tells if a room is encrypted
* Tells if a room is encrypted with MXCRYPTO_ALGORITHM_MEGOLM
*
* @param roomId the room id
* @return true if the room is encrypted
* @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM
*/
override fun isRoomEncrypted(roomId: String): Boolean {
val encryptionEvent = monarchy.fetchCopied {
EventEntity.whereType(it, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION).findFirst()
val encryptionEvent = monarchy.fetchCopied { realm ->
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.findFirst()
}
return encryptionEvent != null
}

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

@ -130,7 +130,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

@ -23,8 +23,12 @@ import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
@ -32,6 +36,8 @@ import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.getOrNull
import im.vector.matrix.android.internal.database.query.isEventRead
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.database.query.whereType
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
@ -94,10 +100,15 @@ internal class RoomSummaryUpdater @Inject constructor(
}
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root
val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root
val lastAliasesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root
val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root
// Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.findFirst()
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
// avoid this call if we are sure there are unread events

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

@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="notice_end_to_end_ok">%1$s turned on end-to-end encryption.</string>
<string name="notice_end_to_end_unknown_algorithm">%1$s turned on end-to-end encryption (unrecognised algorithm %2$s).</string>
</resources>

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,32 +22,49 @@ 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.SASVerificationIncomingFragment
import im.vector.riotx.features.crypto.verification.SASVerificationShortCodeFragment
import im.vector.riotx.features.crypto.verification.SASVerificationStartFragment
import im.vector.riotx.features.crypto.verification.SASVerificationVerifiedFragment
import im.vector.riotx.features.grouplist.GroupListFragment
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
@ -272,6 +289,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

@ -113,3 +113,39 @@ fun containsOnlyEmojis(str: String?): Boolean {
return res
}
/**
* Same as split, but considering emojis
*/
fun CharSequence.splitEmoji(): List<CharSequence> {
val result = mutableListOf<CharSequence>()
var index = 0
while (index < length) {
val firstChar = get(index)
if (firstChar.toInt() == 0x200e) {
// Left to right mark. What should I do with it?
} else if (firstChar.toInt() in 0xD800..0xDBFF && index + 1 < length) {
// We have the start of a surrogate pair
val secondChar = get(index + 1)
if (secondChar.toInt() in 0xDC00..0xDFFF) {
// We have an emoji
result.add("$firstChar$secondChar")
index++
} else {
// Not sure what we have here...
result.add("$firstChar")
}
} else {
// Regular char
result.add("$firstChar")
}
index++
}
return result
}

View file

@ -37,6 +37,8 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
KICK_USER("/kick", "<user-id> [reason]", R.string.command_description_kick_user),
CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick),
MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown),
RAINBOW("/rainbow", "<message>", R.string.command_description_rainbow),
RAINBOW_EMOTE("/rainbowme", "<message>", R.string.command_description_rainbow_emote),
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token),
SPOILER("/spoiler", "<message>", R.string.command_description_spoiler);

View file

@ -80,6 +80,16 @@ object CommandParser {
ParsedCommand.SendEmote(message)
}
Command.RAINBOW.command -> {
val message = textMessage.subSequence(Command.RAINBOW.command.length, textMessage.length).trim()
ParsedCommand.SendRainbow(message)
}
Command.RAINBOW_EMOTE.command -> {
val message = textMessage.subSequence(Command.RAINBOW_EMOTE.command.length, textMessage.length).trim()
ParsedCommand.SendRainbowEmote(message)
}
Command.JOIN_ROOM.command -> {
if (messageParts.size >= 2) {
val roomAlias = messageParts[1]

View file

@ -34,6 +34,8 @@ sealed class ParsedCommand {
// Valid commands:
class SendEmote(val message: CharSequence) : ParsedCommand()
class SendRainbow(val message: CharSequence) : ParsedCommand()
class SendRainbowEmote(val message: CharSequence) : ParsedCommand()
class BanUser(val userId: String, val reason: String?) : ParsedCommand()
class UnbanUser(val userId: String, val reason: String?) : ParsedCommand()
class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand()

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

@ -44,6 +44,12 @@ abstract class FormSwitchItem : VectorEpoxyModel<FormSwitchItem.Holder>() {
var summary: String? = null
override fun bind(holder: Holder) {
holder.view.setOnClickListener {
if (enabled) {
holder.switchView.toggle()
}
}
holder.titleView.text = title
holder.summaryView.setTextOrHide(summary)

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

@ -85,6 +85,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
@ -306,16 +307,13 @@ class RoomDetailFragment @Inject constructor(
displayRoomDetailActionResult(it)
}
roomDetailViewModel.viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
}
}
.disposeOnDestroyView()
roomDetailViewModel.observeViewEvents {
when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
}.exhaustive
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {

View file

@ -16,10 +16,12 @@
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()
data class OnNewTimelineEvents(val eventIds: List<String>) : RoomDetailViewEvents()
}

View file

@ -60,17 +60,17 @@ import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventConten
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.core.utils.DataSource
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.core.utils.NoOpMatrixCallback
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
import im.vector.riotx.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.riotx.features.home.room.typing.TypingHelper
import im.vector.riotx.features.settings.VectorPreferences
@ -90,8 +90,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private val vectorPreferences: VectorPreferences,
private val stringProvider: StringProvider,
private val typingHelper: TypingHelper,
private val rainbowGenerator: RainbowGenerator,
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
@ -114,9 +115,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>>>
@ -317,6 +315,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
@ -411,6 +410,20 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendRainbow -> {
slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
}
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendRainbowEmote -> {
slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
}
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendSpoiler -> {
room.sendFormattedTextMessage(
"[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})",
@ -427,7 +440,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
}
}
}.exhaustive
}
is SendMode.EDIT -> {
// is original event a reply?
@ -485,7 +498,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
popDraft()
}
}
}
}.exhaustive
}
}

View file

@ -0,0 +1,89 @@
/*
* 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.detail.composer.rainbow
import im.vector.riotx.core.utils.splitEmoji
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.roundToInt
/**
* Inspired from React-Sdk
* Ref: https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/utils/colour.js
*/
class RainbowGenerator @Inject constructor() {
fun generate(text: String): String {
val split = text.splitEmoji()
val frequency = 360f / split.size
return split
.mapIndexed { idx, letter ->
// Do better than React-Sdk: Avoid adding font color for spaces
if (letter == " ") {
"$letter"
} else {
val dashColor = hueToRGB(idx * frequency, 1.0f, 0.5f).toDashColor()
"<font color=\"$dashColor\">$letter</font>"
}
}
.joinToString(separator = "")
}
private fun hueToRGB(h: Float, s: Float, l: Float): RgbColor {
val c = s * (1 - abs(2 * l - 1))
val x = c * (1 - abs((h / 60) % 2 - 1))
val m = l - c / 2
var r = 0f
var g = 0f
var b = 0f
when {
h < 60f -> {
r = c
g = x
}
h < 120f -> {
r = x
g = c
}
h < 180f -> {
g = c
b = x
}
h < 240f -> {
g = x
b = c
}
h < 300f -> {
r = x
b = c
}
else -> {
r = c
b = x
}
}
return RgbColor(
((r + m) * 255).roundToInt(),
((g + m) * 255).roundToInt(),
((b + m) * 255).roundToInt()
)
}
}

View file

@ -0,0 +1,30 @@
/*
* 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.detail.composer.rainbow
data class RgbColor(
val r: Int,
val g: Int,
val b: Int
)
fun RgbColor.toDashColor(): String {
return listOf(r, g, b)
.joinToString(separator = "", prefix = "#") {
it.toString(16).padStart(2, '0')
}
}

View file

@ -46,17 +46,14 @@ class MessageActionsEpoxyController @Inject constructor(
override fun buildModels(state: MessageActionState) {
// Message preview
val body = state.messageBody
if (body != null) {
bottomSheetMessagePreviewItem {
id("preview")
avatarRenderer(avatarRenderer)
matrixItem(state.informationData.matrixItem)
movementMethod(createLinkMovementMethod(listener))
userClicked { listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) }
body(body.linkify(listener))
time(state.time())
}
bottomSheetMessagePreviewItem {
id("preview")
avatarRenderer(avatarRenderer)
matrixItem(state.informationData.matrixItem)
movementMethod(createLinkMovementMethod(listener))
userClicked { listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) }
body(state.messageBody.linkify(listener))
time(state.time())
}
// Send state

View file

@ -15,7 +15,12 @@
*/
package im.vector.riotx.features.home.room.detail.timeline.action
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 dagger.Lazy
@ -35,6 +40,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
@ -44,7 +50,8 @@ import im.vector.riotx.features.html.VectorHtmlCompressor
import im.vector.riotx.features.reactions.data.EmojiDataSource
import im.vector.riotx.features.settings.VectorPreferences
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
/**
* Quick reactions state
@ -59,7 +66,7 @@ data class MessageActionState(
val eventId: String,
val informationData: MessageInformationData,
val timelineEvent: Async<TimelineEvent> = Uninitialized,
val messageBody: CharSequence? = null,
val messageBody: CharSequence = "",
// For quick reactions
val quickStates: Async<List<ToggleState>> = Uninitialized,
// For actions
@ -89,7 +96,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
@ -154,13 +161,16 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun observeTimelineEventState() {
asyncSubscribe(MessageActionState::timelineEvent) { timelineEvent ->
val computedMessage = computeMessageBody(timelineEvent)
val actions = actionsForEvent(timelineEvent)
setState { copy(messageBody = computedMessage, actions = actions) }
setState {
copy(
messageBody = computeMessageBody(timelineEvent),
actions = actionsForEvent(timelineEvent)
)
}
}
}
private fun computeMessageBody(timelineEvent: TimelineEvent): CharSequence? {
private fun computeMessageBody(timelineEvent: TimelineEvent): CharSequence {
return when (timelineEvent.root.getClearType()) {
EventType.MESSAGE,
EventType.STICKER -> {
@ -188,7 +198,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
noticeEventFormatter.format(timelineEvent)
}
else -> null
}
} ?: ""
}
private fun actionsForEvent(timelineEvent: TimelineEvent): List<EventSharedAction> {

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,10 +16,13 @@
package im.vector.riotx.features.home.room.detail.timeline.factory
import android.view.View
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem
import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem_
@ -28,20 +31,26 @@ import javax.inject.Inject
class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: AvatarSizeProvider,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val informationDataFactory: MessageInformationDataFactory) {
fun create(text: String,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): DefaultItem {
val attributes = DefaultItem.Attributes(
avatarRenderer = avatarRenderer,
informationData = informationData,
text = text,
itemLongClickListener = View.OnLongClickListener { view ->
callback?.onEventLongClicked(informationData, null, view) ?: false
},
readReceiptsCallback = callback
)
return DefaultItem_()
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(highlight)
.text(text)
.avatarRenderer(avatarRenderer)
.informationData(informationData)
.baseCallback(callback)
.readReceiptsCallback(callback)
.attributes(attributes)
}
fun create(event: TimelineEvent,
@ -49,9 +58,9 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
callback: TimelineEventController.Callback?,
throwable: Throwable? = null): DefaultItem {
val text = if (throwable == null) {
"${event.root.getClearType()} events are not yet handled"
stringProvider.getString(R.string.rendering_event_error_type_of_event_not_handled, event.root.getClearType())
} else {
"an exception occurred when rendering the event ${event.root.eventId}"
stringProvider.getString(R.string.rendering_event_error_exception, event.root.eventId)
}
val informationData = informationDataFactory.create(event, null)
return create(text, informationData, highlight, callback)

View file

@ -26,7 +26,16 @@ import android.view.View
import dagger.Lazy
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
@ -40,8 +49,24 @@ import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.core.utils.containsOnlyEmojis
import im.vector.riotx.core.utils.isLocalFile
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.*
import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageBlockCodeItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageBlockCodeItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem
import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem_
import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.riotx.features.home.room.detail.timeline.tools.linkify
import im.vector.riotx.features.html.CodeVisitor
@ -153,7 +178,7 @@ class MessageItemFactory @Inject constructor(
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): DefaultItem? {
val text = "${messageContent.type} message events are not yet handled"
val text = stringProvider.getString(R.string.rendering_event_error_type_of_message_not_handled, messageContent.type)
return defaultItemFactory.create(text, informationData, highlight, callback)
}

View file

@ -19,9 +19,21 @@ package im.vector.riotx.features.home.room.detail.timeline.format
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.*
import im.vector.matrix.android.api.session.room.model.GuestAccess
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomGuestAccessContent
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomJoinRules
import im.vector.matrix.android.api.session.room.model.RoomJoinRulesContent
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
@ -180,7 +192,11 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
private fun formatRoomEncryptionEvent(event: Event, senderName: String?): CharSequence? {
val content = event.content.toModel<EncryptionEventContent>() ?: return null
return sp.getString(R.string.notice_end_to_end, senderName, content.algorithm)
return if (content.algorithm == MXCRYPTO_ALGORITHM_MEGOLM) {
sp.getString(R.string.notice_end_to_end_ok, senderName)
} else {
sp.getString(R.string.notice_end_to_end_unknown_algorithm, senderName, content.algorithm)
}
}
private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?): String {

View file

@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item
import android.view.View
import android.view.ViewStub
import android.widget.RelativeLayout
import androidx.annotation.CallSuper
import androidx.annotation.IdRes
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
@ -42,6 +43,7 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
@EpoxyAttribute
lateinit var dimensionConverter: DimensionConverter
@CallSuper
override fun bind(holder: H) {
super.bind(holder)
holder.leftGuideline.updateLayoutParams<RelativeLayout.LayoutParams> {

View file

@ -17,6 +17,7 @@
package im.vector.riotx.features.home.room.detail.timeline.item
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
@ -29,42 +30,39 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle
abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
@EpoxyAttribute
lateinit var informationData: MessageInformationData
@EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute
var baseCallback: TimelineEventController.BaseCallback? = null
private var longClickListener = View.OnLongClickListener {
return@OnLongClickListener baseCallback?.onEventLongClicked(informationData, null, it) == true
}
@EpoxyAttribute
var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
lateinit var attributes: Attributes
private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts)
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
})
@EpoxyAttribute
var text: CharSequence? = null
override fun bind(holder: Holder) {
holder.messageView.text = text
holder.view.setOnLongClickListener(longClickListener)
holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener)
super.bind(holder)
holder.messageTextView.text = attributes.text
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
}
override fun getEventIds(): List<String> {
return listOf(informationData.eventId)
return listOf(attributes.informationData.eventId)
}
override fun getViewType() = STUB_ID
class Holder : BaseHolder(STUB_ID) {
val messageView by bind<TextView>(R.id.stateMessageView)
val avatarImageView by bind<ImageView>(R.id.itemDefaultAvatarView)
val messageTextView by bind<TextView>(R.id.itemDefaultTextView)
}
data class Attributes(
val avatarRenderer: AvatarRenderer,
val informationData: MessageInformationData,
val text: CharSequence,
val itemLongClickListener: View.OnLongClickListener? = null,
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
)
companion object {
private const val STUB_ID = R.id.messageContentDefaultStub
}

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

@ -22,5 +22,6 @@ sealed class CreateRoomAction : VectorViewModelAction {
data class SetName(val name: String) : CreateRoomAction()
data class SetIsPublic(val isPublic: Boolean) : CreateRoomAction()
data class SetIsInRoomDirectory(val isInRoomDirectory: Boolean) : CreateRoomAction()
data class SetIsEncrypted(val isEncrypted: Boolean) : CreateRoomAction()
object Create : CreateRoomAction()
}

View file

@ -39,9 +39,7 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
var index = 0
override fun buildModels(viewState: CreateRoomViewState) {
val asyncCreateRoom = viewState.asyncCreateRoomRequest
when (asyncCreateRoom) {
when (val asyncCreateRoom = viewState.asyncCreateRoomRequest) {
is Success -> {
// Nothing to display, the screen will be closed
}
@ -101,12 +99,24 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
listener?.setIsInRoomDirectory(value)
}
}
formSwitchItem {
id("encryption")
enabled(enableFormElement)
title(stringProvider.getString(R.string.create_room_encryption_title))
summary(stringProvider.getString(R.string.create_room_encryption_description))
switchChecked(viewState.isEncrypted)
listener { value ->
listener?.setIsEncrypted(value)
}
}
}
interface Listener {
fun onNameChange(newName: String)
fun setIsPublic(isPublic: Boolean)
fun setIsInRoomDirectory(isInRoomDirectory: Boolean)
fun setIsEncrypted(isEncrypted: Boolean)
fun retry()
}
}

View file

@ -85,6 +85,10 @@ class CreateRoomFragment @Inject constructor(private val createRoomController: C
viewModel.handle(CreateRoomAction.SetIsInRoomDirectory(isInRoomDirectory))
}
override fun setIsEncrypted(isEncrypted: Boolean) {
viewModel.handle(CreateRoomAction.SetIsEncrypted(isEncrypted))
}
override fun retry() {
Timber.v("Retry")
viewModel.handle(CreateRoomAction.Create)

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,14 @@ 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.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
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 {
@ -56,6 +63,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
is CreateRoomAction.SetName -> setName(action)
is CreateRoomAction.SetIsPublic -> setIsPublic(action)
is CreateRoomAction.SetIsInRoomDirectory -> setIsInRoomDirectory(action)
is CreateRoomAction.SetIsEncrypted -> setIsEncrypted(action)
is CreateRoomAction.Create -> doCreateRoom()
}
}
@ -66,6 +74,8 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
private fun setIsInRoomDirectory(action: CreateRoomAction.SetIsInRoomDirectory) = setState { copy(isInRoomDirectory = action.isInRoomDirectory) }
private fun setIsEncrypted(action: CreateRoomAction.SetIsEncrypted) = setState { copy(isEncrypted = action.isEncrypted) }
private fun doCreateRoom() = withState { state ->
if (state.asyncCreateRoomRequest is Loading || state.asyncCreateRoomRequest is Success) {
return@withState
@ -83,6 +93,11 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
// Public room
preset = if (state.isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT
// Encryption
if (state.isEncrypted) {
enableEncryptionWithAlgorithm(MXCRYPTO_ALGORITHM_MEGOLM)
}
}
session.createRoom(createRoomParams, object : MatrixCallback<String> {

View file

@ -24,5 +24,6 @@ data class CreateRoomViewState(
val roomName: String = "",
val isPublic: Boolean = false,
val isInRoomDirectory: Boolean = false,
val isEncrypted: Boolean = false,
val asyncCreateRoomRequest: Async<String> = Uninitialized
) : MvRxState

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,15 @@ 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 roomMemberSummaryComparator: RoomMemberSummaryComparator,
private val session: Session)
: VectorViewModel<RoomMemberListViewState, RoomMemberListAction>(initialState) {
: VectorViewModel<RoomMemberListViewState, RoomMemberListAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -112,11 +114,11 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
}
return listOf(
PowerLevelCategory.ADMIN to admins,
PowerLevelCategory.MODERATOR to moderators,
PowerLevelCategory.CUSTOM to customs,
PowerLevelCategory.INVITE to invites,
PowerLevelCategory.USER to users
PowerLevelCategory.ADMIN to admins.sortedWith(roomMemberSummaryComparator),
PowerLevelCategory.MODERATOR to moderators.sortedWith(roomMemberSummaryComparator),
PowerLevelCategory.CUSTOM to customs.sortedWith(roomMemberSummaryComparator),
PowerLevelCategory.INVITE to invites.sortedWith(roomMemberSummaryComparator),
PowerLevelCategory.USER to users.sortedWith(roomMemberSummaryComparator)
)
}

View file

@ -0,0 +1,61 @@
/*
* 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.members
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import javax.inject.Inject
class RoomMemberSummaryComparator @Inject constructor() : Comparator<RoomMemberSummary> {
override fun compare(leftRoomMemberSummary: RoomMemberSummary?, rightRoomMemberSummary: RoomMemberSummary?): Int {
return when (leftRoomMemberSummary) {
null ->
when (rightRoomMemberSummary) {
null -> 0
else -> 1
}
else ->
when (rightRoomMemberSummary) {
null -> -1
else ->
when {
leftRoomMemberSummary.displayName.isNullOrBlank() ->
when {
rightRoomMemberSummary.displayName.isNullOrBlank() -> {
// No display names, compare ids
leftRoomMemberSummary.userId.compareTo(rightRoomMemberSummary.userId)
}
else -> 1
}
else ->
when {
rightRoomMemberSummary.displayName.isNullOrBlank() -> -1
else -> {
when (leftRoomMemberSummary.displayName) {
rightRoomMemberSummary.displayName ->
// Same display name, compare id
leftRoomMemberSummary.userId.compareTo(rightRoomMemberSummary.userId)
else ->
leftRoomMemberSummary.displayName!!.compareTo(rightRoomMemberSummary.displayName!!, true)
}
}
}
}
}
}
}
}

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" />
<im.vector.riotx.core.platform.EllipsizingTextView
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>

Some files were not shown because too many files have changed in this diff Show more