Merge pull request #604 from vector-im/feature/performance

Feature/performance
This commit is contained in:
ganfra 2019-10-07 16:08:39 +02:00 committed by GitHub
commit 4f7ec91255
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 415 additions and 511 deletions

View File

@ -0,0 +1,24 @@
/*
* 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.matrix.rx
import im.vector.matrix.android.api.util.Optional
import io.reactivex.Observable
fun <T : Any> Observable<Optional<T>>.unwrap(): Observable<T> {
return filter { it.hasValue() }.map { it.get() }
}

View File

@ -28,7 +28,7 @@ import io.reactivex.Single
class RxRoom(private val room: Room) { class RxRoom(private val room: Room) {
fun liveRoomSummary(): Observable<RoomSummary> { fun liveRoomSummary(): Observable<Optional<RoomSummary>> {
return room.getRoomSummaryLive().asObservable() return room.getRoomSummaryLive().asObservable()
} }
@ -36,11 +36,11 @@ class RxRoom(private val room: Room) {
return room.getRoomMemberIdsLive().asObservable() return room.getRoomMemberIdsLive().asObservable()
} }
fun liveAnnotationSummary(eventId: String): Observable<EventAnnotationsSummary> { fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> {
return room.getEventSummaryLive(eventId).asObservable() return room.getEventSummaryLive(eventId).asObservable()
} }
fun liveTimelineEvent(eventId: String): Observable<TimelineEvent> { fun liveTimelineEvent(eventId: String): Observable<Optional<TimelineEvent>> {
return room.getTimeLineEventLive(eventId).asObservable() return room.getTimeLineEventLive(eventId).asObservable()
} }

View File

@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.util.Optional
/** /**
* This interface defines methods to interact within a room. * This interface defines methods to interact within a room.
@ -49,7 +50,7 @@ interface Room :
* A live [RoomSummary] associated with the room * A live [RoomSummary] associated with the room
* You can observe this summary to get dynamic data from this room. * You can observe this summary to get dynamic data from this room.
*/ */
fun getRoomSummaryLive(): LiveData<RoomSummary> fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>>
fun roomSummary(): RoomSummary? fun roomSummary(): RoomSummary?

View File

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
/** /**
* In some cases, events may wish to reference other events. * In some cases, events may wish to reference other events.
@ -111,7 +112,7 @@ interface RelationService {
replyText: String, replyText: String,
autoMarkdown: Boolean = false): Cancelable? autoMarkdown: Boolean = false): Cancelable?
fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary> fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
} }

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.room.timeline package im.vector.matrix.android.api.session.room.timeline
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.util.Optional
/** /**
* This interface defines methods to interact with the timeline. It's implemented at the room level. * This interface defines methods to interact with the timeline. It's implemented at the room level.
@ -36,5 +37,5 @@ interface TimelineService {
fun getTimeLineEvent(eventId: String): TimelineEvent? fun getTimeLineEvent(eventId: String): TimelineEvent?
fun getTimeLineEventLive(eventId: String): LiveData<TimelineEvent> fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
} }

View File

@ -31,6 +31,10 @@ data class Optional<T : Any> constructor(private val value: T?) {
return value ?: fn() return value ?: fn()
} }
fun hasValue(): Boolean{
return value != null
}
companion object { companion object {
fun <T : Any> from(value: T?): Optional<T> { fun <T : Any> from(value: T?): Optional<T> {
return Optional(value) return Optional(value)

View File

@ -39,9 +39,10 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
internal class DefaultAuthenticator @Inject constructor(@Unauthenticated internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
private val okHttpClient: OkHttpClient, private val okHttpClient: Provider<OkHttpClient>,
private val retrofitFactory: RetrofitFactory, private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionParamsStore: SessionParamsStore, private val sessionParamsStore: SessionParamsStore,
@ -119,7 +120,7 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
} }
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) val retrofit = retrofitFactory.create(okHttpClient.get(), homeServerConnectionConfig.homeServerUri.toString())
return retrofit.create(AuthAPI::class.java) return retrofit.create(AuthAPI::class.java)
} }

View File

@ -1,46 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database
import androidx.lifecycle.LiveData
import io.realm.*
class RealmLiveData<T : RealmModel>(private val realmConfiguration: RealmConfiguration,
private val query: (Realm) -> RealmQuery<T>) : LiveData<RealmResults<T>>() {
private val listener = RealmChangeListener<RealmResults<T>> { results ->
value = results
}
private var realm: Realm? = null
private var results: RealmResults<T>? = null
override fun onActive() {
val realm = Realm.getInstance(realmConfiguration)
val results = query.invoke(realm).findAllAsync()
results.addChangeListener(listener)
this.realm = realm
this.results = results
}
override fun onInactive() {
results?.removeChangeListener(listener)
results = null
realm?.close()
realm = null
}
}

View File

@ -29,7 +29,8 @@ import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity 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.model.RoomSummaryEntityFields
@ -56,19 +57,13 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
RelationService by relationService, RelationService by relationService,
MembershipService by roomMembersService { MembershipService by roomMembersService {
override fun getRoomSummaryLive(): LiveData<RoomSummary> { override fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>> {
val liveRealmData = RealmLiveData<RoomSummaryEntity>(monarchy.realmConfiguration) { realm -> val liveData = monarchy.findAllMappedWithChanges(
RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) { realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) },
} { roomSummaryMapper.map(it) }
return Transformations.map(liveRealmData) { results -> )
val roomSummaries = results.map { roomSummaryMapper.map(it) } return Transformations.map(liveData) { results ->
results.firstOrNull().toOptional()
if (roomSummaries.isEmpty()) {
// Create a dummy RoomSummary to avoid Crash during Sign Out or clear cache
RoomSummary(roomId)
} else {
roomSummaries.first()
}
} }
} }

View File

@ -24,7 +24,6 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.DraftMapper import im.vector.matrix.android.internal.database.mapper.DraftMapper
import im.vector.matrix.android.internal.database.model.DraftEntity import im.vector.matrix.android.internal.database.model.DraftEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
@ -51,12 +50,13 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private
monarchy.writeAsync { realm -> monarchy.writeAsync { realm ->
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
val userDraftsEntity = roomSummaryEntity.userDrafts val userDraftsEntity = roomSummaryEntity.userDrafts
?: realm.createObject<UserDraftsEntity>().also { ?: realm.createObject<UserDraftsEntity>().also {
roomSummaryEntity.userDrafts = it roomSummaryEntity.userDrafts = it
} }
userDraftsEntity.let { userDraftEntity -> userDraftsEntity.let { userDraftEntity ->
// Save only valid draft // Save only valid draft
@ -150,16 +150,16 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private
} }
override fun getDraftsLive(): LiveData<List<UserDraft>> { override fun getDraftsLive(): LiveData<List<UserDraft>> {
val liveData = RealmLiveData(monarchy.realmConfiguration) { val liveData = monarchy.findAllMappedWithChanges(
UserDraftsEntity.where(it, roomId) { UserDraftsEntity.where(it, roomId) },
} {
it.userDrafts.map { draft ->
return Transformations.map(liveData) { userDraftsEntities -> DraftMapper.map(draft)
userDraftsEntities.firstOrNull()?.let { userDraftEntity -> }
userDraftEntity.userDrafts.map { draftEntity ->
DraftMapper.map(draftEntity)
} }
} ?: emptyList() )
return Transformations.map(liveData) {
it.firstOrNull() ?: emptyList()
} }
} }
} }

View File

@ -25,7 +25,7 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
@ -82,33 +82,33 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private
} }
override fun getReadMarkerLive(): LiveData<Optional<String>> { override fun getReadMarkerLive(): LiveData<Optional<String>> {
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> val liveRealmData = monarchy.findAllMappedWithChanges(
ReadMarkerEntity.where(realm, roomId) { ReadMarkerEntity.where(it, roomId) },
} { it.eventId }
return Transformations.map(liveRealmData) { results -> )
Optional.from(results.firstOrNull()?.eventId) return Transformations.map(liveRealmData) {
it.firstOrNull().toOptional()
} }
} }
override fun getMyReadReceiptLive(): LiveData<Optional<String>> { override fun getMyReadReceiptLive(): LiveData<Optional<String>> {
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> val liveRealmData = monarchy.findAllMappedWithChanges(
ReadReceiptEntity.where(realm, roomId = roomId, userId = userId) { ReadReceiptEntity.where(it, roomId = roomId, userId = userId) },
} { it.eventId }
return Transformations.map(liveRealmData) { results -> )
Optional.from(results.firstOrNull()?.eventId) return Transformations.map(liveRealmData) {
it.firstOrNull().toOptional()
} }
} }
override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> { override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> {
val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm ->
ReadReceiptsSummaryEntity.where(realm, eventId) val liveRealmData = monarchy.findAllMappedWithChanges(
} { ReadReceiptsSummaryEntity.where(it, eventId) },
return Transformations.map(liveEntity) { realmResults -> { readReceiptsSummaryMapper.map(it) }
realmResults.firstOrNull()?.let { )
readReceiptsSummaryMapper.map(it) return Transformations.map(liveRealmData) {
}?.sortedByDescending { it.firstOrNull() ?: emptyList()
it.originServerTs
} ?: emptyList()
} }
} }
} }

View File

@ -30,7 +30,8 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.helper.addSendingEvent import im.vector.matrix.android.internal.database.helper.addSendingEvent
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
@ -210,13 +211,13 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain) return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
} }
override fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary> { override fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>> {
val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm -> val liveData = monarchy.findAllMappedWithChanges(
EventAnnotationsSummaryEntity.where(realm, eventId) { EventAnnotationsSummaryEntity.where(it, eventId)},
} { it.asDomain() }
return Transformations.map(liveEntity) { realmResults -> )
realmResults.firstOrNull()?.asDomain() return Transformations.map(liveData) { results ->
?: EventAnnotationsSummary(eventId, emptyList(), null) results.firstOrNull().toOptional()
} }
} }

View File

@ -26,7 +26,8 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity
@ -75,12 +76,13 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
}) })
} }
override fun getTimeLineEventLive(eventId: String): LiveData<TimelineEvent> { override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
val liveData = RealmLiveData(monarchy.realmConfiguration) { val liveData = monarchy.findAllMappedWithChanges(
TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) { TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) },
} { timelineEventMapper.map(it) }
)
return Transformations.map(liveData) { events -> return Transformations.map(liveData) { events ->
events.firstOrNull()?.let { timelineEventMapper.map(it) } events.firstOrNull().toOptional()
} }
} }

View File

@ -28,7 +28,6 @@ import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.database.model.UserEntityFields import im.vector.matrix.android.internal.database.model.UserEntityFields
@ -63,20 +62,18 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
override fun getUser(userId: String): User? { override fun getUser(userId: String): User? {
val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() }
?: return null ?: return null
return userEntity.asDomain() return userEntity.asDomain()
} }
override fun liveUser(userId: String): LiveData<Optional<User>> { override fun liveUser(userId: String): LiveData<Optional<User>> {
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> val liveData = monarchy.findAllMappedWithChanges(
UserEntity.where(realm, userId) { UserEntity.where(it, userId) },
} { it.asDomain() }
return Transformations.map(liveRealmData) { results -> )
results return Transformations.map(liveData) { results ->
.map { it.asDomain() } results.firstOrNull().toOptional()
.firstOrNull()
.toOptional()
} }
} }

View File

@ -234,7 +234,9 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1'
implementation 'androidx.core:core-ktx:1.0.2' implementation 'androidx.core:core-ktx:1.0.2'
implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1' implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0"
implementation "com.squareup.moshi:moshi-adapters:$moshi_version" implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
@ -280,6 +282,7 @@ dependencies {
implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:core:$markwon_version"
implementation "ru.noties.markwon:html:$markwon_version" implementation "ru.noties.markwon:html:$markwon_version"
implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.google.android:flexbox:1.1.1'
// Bus // Bus
implementation 'org.greenrobot:eventbus:3.1.1' implementation 'org.greenrobot:eventbus:3.1.1'

View File

@ -354,8 +354,11 @@ SOFTWARE.
<br/> <br/>
Copyright (C) 2012-2017 Markus Junginger, greenrobot (http://greenrobot.org) Copyright (C) 2012-2017 Markus Junginger, greenrobot (http://greenrobot.org)
</li> </li>
<li>
<b>LazyThreeTenBp</b>
<br/>
Copyright 2017 Gabriel Ittner.
</li>
</ul> </ul>
<pre> <pre>
Apache License Apache License

View File

@ -31,9 +31,9 @@ import androidx.multidex.MultiDex
import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.facebook.stetho.Stetho import com.facebook.stetho.Stetho
import com.gabrielittner.threetenbp.LazyThreeTen
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.glide.GlideImageLoader import com.github.piasy.biv.loader.glide.GlideImageLoader
import com.jakewharton.threetenabp.AndroidThreeTen
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixConfiguration import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.auth.Authenticator
@ -96,7 +96,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
Stetho.initializeWithDefaults(this) Stetho.initializeWithDefaults(this)
} }
logInfo() logInfo()
AndroidThreeTen.init(this) LazyThreeTen.init(this)
BigImageViewer.initialize(GlideImageLoader.with(applicationContext)) BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()

View File

@ -70,7 +70,7 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback
is Incomplete -> stateView.state = StateView.State.Loading is Incomplete -> stateView.state = StateView.State.Loading
is Success -> stateView.state = StateView.State.Content is Success -> stateView.state = StateView.State.Content
} }
groupController.setData(state) groupController.update(state)
} }
override fun onGroupSelected(groupSummary: GroupSummary) { override fun onGroupSelected(groupSummary: GroupSummary) {

View File

@ -16,17 +16,29 @@
package im.vector.riotx.features.home.group package im.vector.riotx.features.home.group
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject import javax.inject.Inject
class GroupSummaryController @Inject constructor(private val avatarRenderer: AvatarRenderer): TypedEpoxyController<GroupListViewState>() { class GroupSummaryController @Inject constructor(private val avatarRenderer: AvatarRenderer) : EpoxyController() {
var callback: Callback? = null var callback: Callback? = null
private var viewState: GroupListViewState? = null
override fun buildModels(viewState: GroupListViewState) { init {
buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup) requestModelBuild()
}
fun update(viewState: GroupListViewState) {
this.viewState = viewState
requestModelBuild()
}
override fun buildModels() {
val nonNullViewState = viewState ?: return
buildGroupModels(nonNullViewState.asyncGroups(), nonNullViewState.selectedGroup)
} }
private fun buildGroupModels(summaries: List<GroupSummary>?, selected: GroupSummary?) { private fun buildGroupModels(summaries: List<GroupSummary>?, selected: GroupSummary?) {

View File

@ -50,6 +50,7 @@ import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.extensions.postLiveEvent
@ -719,6 +720,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun observeRoomSummary() { private fun observeRoomSummary() {
room.rx().liveRoomSummary() room.rx().liveRoomSummary()
.unwrap()
.execute { async -> .execute { async ->
copy( copy(
asyncRoomSummary = async, asyncRoomSummary = async,

View File

@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent 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.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.rx.RxRoom import im.vector.matrix.rx.RxRoom
import im.vector.matrix.rx.unwrap
import im.vector.riotx.core.extensions.canReact import im.vector.riotx.core.extensions.canReact
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
@ -51,7 +52,7 @@ data class MessageActionState(
fun senderName(): String = informationData.memberName?.toString() ?: "" fun senderName(): String = informationData.memberName?.toString() ?: ""
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) }
?: "" ?: ""
fun canReact() = timelineEvent()?.canReact() == true fun canReact() = timelineEvent()?.canReact() == true
@ -61,7 +62,7 @@ data class MessageActionState(
val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent() val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
eventHtmlRenderer?.render(messageContent.formattedBody eventHtmlRenderer?.render(messageContent.formattedBody
?: messageContent.body) ?: messageContent.body)
} else { } else {
messageContent?.body messageContent?.body
} }
@ -116,6 +117,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
if (room == null) return if (room == null) return
RxRoom(room) RxRoom(room)
.liveTimelineEvent(eventId) .liveTimelineEvent(eventId)
.unwrap()
.execute { .execute {
copy(timelineEvent = it) copy(timelineEvent = it)
} }

View File

@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.rx.RxRoom import im.vector.matrix.rx.RxRoom
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.canReact import im.vector.riotx.core.extensions.canReact
@ -110,7 +111,9 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
} }
} }
private fun actionsForEvent(event: TimelineEvent): List<SimpleAction> { private fun actionsForEvent(optionalEvent: Optional<TimelineEvent>): List<SimpleAction> {
val event = optionalEvent.getOrNull() ?: return emptyList()
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel() val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
?: event.root.getClearContent().toModel() ?: event.root.getClearContent().toModel()
val type = messageContent?.type val type = messageContent?.type

View File

@ -77,7 +77,7 @@ class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState:
.liveAnnotationSummary(eventId) .liveAnnotationSummary(eventId)
.map { annotations -> .map { annotations ->
quickEmojis.map { emoji -> quickEmojis.map { emoji ->
ToggleState(emoji, annotations.reactionsSummary.firstOrNull { it.key == emoji }?.addedByMe ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe
?: false) ?: false)
} }
} }

View File

@ -27,6 +27,7 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary
import im.vector.matrix.rx.RxRoom import im.vector.matrix.rx.RxRoom
import im.vector.matrix.rx.unwrap
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.isSingleEmoji import im.vector.riotx.core.utils.isSingleEmoji
import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.date.VectorDateFormatter
@ -87,6 +88,7 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
private fun observeEventAnnotationSummaries() { private fun observeEventAnnotationSummaries() {
RxRoom(room) RxRoom(room)
.liveAnnotationSummary(eventId) .liveAnnotationSummary(eventId)
.unwrap()
.flatMapSingle { summaries -> .flatMapSingle { summaries ->
Observable Observable
.fromIterable(summaries.reactionsSummary) .fromIterable(summaries.reactionsSummary)

View File

@ -250,6 +250,7 @@ class MessageItemFactory @Inject constructor(
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? { attributes: AbsMessageItem.Attributes): MessageTextItem? {
val isFormatted = messageContent.formattedBody.isNullOrBlank().not()
val bodyToUse = messageContent.formattedBody?.let { val bodyToUse = messageContent.formattedBody?.let {
htmlRenderer.get().render(it.trim()) htmlRenderer.get().render(it.trim())
} ?: messageContent.body } ?: messageContent.body
@ -265,6 +266,7 @@ class MessageItemFactory @Inject constructor(
message(linkifiedBody) message(linkifiedBody)
} }
} }
.searchForPills(isFormatted)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes) .attributes(attributes)
.highlighted(highlight) .highlighted(highlight)

View File

@ -17,16 +17,11 @@
package im.vector.riotx.features.home.room.detail.timeline.item package im.vector.riotx.features.home.room.detail.timeline.item
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Build
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewStub
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.constraintlayout.helper.widget.Flow
import androidx.core.view.children
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
@ -122,38 +117,23 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
_readMarkerCallback _readMarkerCallback
) )
if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) { val reactions = attributes.informationData.orderedReactionList
holder.reactionWrapper?.isVisible = false if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
holder.reactionsContainer.isVisible = false
} else { } else {
//inflate if needed holder.reactionsContainer.isVisible = true
if (holder.reactionFlowHelper == null) { holder.reactionsContainer.removeAllViews()
holder.reactionWrapper = holder.view.findViewById<ViewStub>(R.id.messageBottomInfo).inflate() as? ViewGroup reactions.take(8).forEach { reaction ->
holder.reactionFlowHelper = holder.view.findViewById(R.id.reactionsFlowHelper) val reactionButton = ReactionButton(holder.view.context)
reactionButton.reactedListener = reactionClickListener
reactionButton.setTag(R.id.reactionsContainer, reaction.key)
reactionButton.reactionString = reaction.key
reactionButton.reactionCount = reaction.count
reactionButton.setChecked(reaction.addedByMe)
reactionButton.isEnabled = reaction.synced
holder.reactionsContainer.addView(reactionButton)
} }
holder.reactionWrapper?.isVisible = true holder.reactionsContainer.setOnLongClickListener(attributes.itemLongClickListener)
//clear all reaction buttons (but not the Flow helper!)
holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true }
val idToRefInFlow = ArrayList<Int>()
attributes.informationData.orderedReactionList?.chunked(8)?.firstOrNull()?.forEachIndexed { index, reaction ->
(holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
reactionButton.isVisible = true
reactionButton.reactedListener = reactionClickListener
reactionButton.setTag(R.id.messageBottomInfo, reaction.key)
idToRefInFlow.add(reactionButton.id)
reactionButton.reactionString = reaction.key
reactionButton.reactionCount = reaction.count
reactionButton.setChecked(reaction.addedByMe)
reactionButton.isEnabled = reaction.synced
}
}
// Just setting the view as gone will break the FlowHelper (and invisible will take too much space),
// so have to update ref ids
holder.reactionFlowHelper?.referencedIds = idToRefInFlow.toIntArray()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) {
holder.reactionFlowHelper?.requestLayout()
}
holder.reactionWrapper?.setOnLongClickListener(attributes.itemLongClickListener)
} }
} }
@ -181,8 +161,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView) val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
val memberNameView by bind<TextView>(R.id.messageMemberNameView) val memberNameView by bind<TextView>(R.id.messageMemberNameView)
val timeView by bind<TextView>(R.id.messageTimeView) val timeView by bind<TextView>(R.id.messageTimeView)
var reactionWrapper: ViewGroup? = null val reactionsContainer by bind<ViewGroup>(R.id.reactionsContainer)
var reactionFlowHelper: Flow? = null
} }
/** /**

View File

@ -17,8 +17,11 @@ package im.vector.riotx.features.home.room.detail.timeline.item
import android.view.View import android.view.View
import android.view.ViewStub import android.view.ViewStub
import android.widget.RelativeLayout
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.constraintlayout.widget.Guideline import androidx.constraintlayout.widget.Guideline
import androidx.core.view.marginStart
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
@ -44,7 +47,9 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder) super.bind(holder)
holder.leftGuideline.setGuidelineBegin(leftGuideline) holder.leftGuideline.updateLayoutParams<RelativeLayout.LayoutParams> {
this.marginStart = leftGuideline
}
holder.checkableBackground.isChecked = highlighted holder.checkableBackground.isChecked = highlighted
} }
@ -55,7 +60,7 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
abstract fun getEventIds(): List<String> abstract fun getEventIds(): List<String>
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
val leftGuideline by bind<Guideline>(R.id.messageStartGuideline) val leftGuideline by bind<View>(R.id.messageStartGuideline)
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground) val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView) val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView) val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)

View File

@ -35,6 +35,8 @@ import me.saket.bettermovementmethod.BetterLinkMovementMethod
@EpoxyModelClass(layout = R.layout.item_timeline_event_base) @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() { abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
@EpoxyAttribute
var searchForPills: Boolean = false
@EpoxyAttribute @EpoxyAttribute
var message: CharSequence? = null var message: CharSequence? = null
@EpoxyAttribute @EpoxyAttribute
@ -65,22 +67,22 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.messageView.movementMethod = mvmtMethod holder.messageView.movementMethod = mvmtMethod
if (useBigFont) { if (useBigFont) {
holder.messageView.textSize = 44F holder.messageView.textSize = 44F
} else { } else {
holder.messageView.textSize = 14F holder.messageView.textSize = 14F
} }
val textFuture = PrecomputedTextCompat.getTextFuture(message ?: "",
TextViewCompat.getTextMetricsParams(holder.messageView),
null)
holder.messageView.setTextFuture(textFuture)
renderSendState(holder.messageView, holder.messageView) renderSendState(holder.messageView, holder.messageView)
holder.messageView.setOnClickListener(attributes.itemClickListener) holder.messageView.setOnClickListener(attributes.itemClickListener)
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener) holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
findPillsAndProcess { it.bind(holder.messageView) } if (searchForPills) {
findPillsAndProcess { it.bind(holder.messageView) }
}
val textFuture = PrecomputedTextCompat.getTextFuture(
message ?: "",
TextViewCompat.getTextMetricsParams(holder.messageView),
null)
holder.messageView.setTextFuture(textFuture)
} }
private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) { private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) {

View File

@ -41,6 +41,7 @@ import im.vector.riotx.features.home.room.list.widget.FabMenuView
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_list.* import kotlinx.android.synthetic.main.fragment_room_list.*
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@Parcelize @Parcelize
@ -180,7 +181,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
is Success -> renderSuccess(state) is Success -> renderSuccess(state)
is Fail -> renderFailure(state.asyncFilteredRooms.error) is Fail -> renderFailure(state.asyncFilteredRooms.error)
} }
roomController.setData(state) roomController.update(state)
} }
private fun renderSuccess(state: RoomListViewState) { private fun renderSuccess(state: RoomListViewState) {

View File

@ -17,40 +17,56 @@
package im.vector.riotx.features.home.room.list package im.vector.riotx.features.home.room.list
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.room.model.Membership 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.RoomSummary
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.filtered.FilteredRoomFooterItem import im.vector.riotx.features.home.room.filtered.FilteredRoomFooterItem
import im.vector.riotx.features.home.room.filtered.filteredRoomFooterItem import im.vector.riotx.features.home.room.filtered.filteredRoomFooterItem
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class RoomSummaryController @Inject constructor(private val stringProvider: StringProvider, class RoomSummaryController @Inject constructor(private val stringProvider: StringProvider,
private val roomSummaryItemFactory: RoomSummaryItemFactory, private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val roomListNameFilter: RoomListNameFilter private val roomListNameFilter: RoomListNameFilter
) : TypedEpoxyController<RoomListViewState>() { ) : EpoxyController() {
var listener: Listener? = null var listener: Listener? = null
override fun buildModels(viewState: RoomListViewState) { private var viewState: RoomListViewState? = null
if (viewState.displayMode == RoomListFragment.DisplayMode.FILTERED) {
buildFilteredRooms(viewState) init {
// We are requesting a model build directly as the first build of epoxy is on the main thread.
// It avoids to build the the whole list of rooms on the main thread.
requestModelBuild()
}
fun update(viewState: RoomListViewState) {
this.viewState = viewState
requestModelBuild()
}
override fun buildModels() {
val nonNullViewState = viewState ?: return
if (nonNullViewState.displayMode == RoomListFragment.DisplayMode.FILTERED) {
buildFilteredRooms(nonNullViewState)
} else { } else {
val roomSummaries = viewState.asyncFilteredRooms() val roomSummaries = nonNullViewState.asyncFilteredRooms()
roomSummaries?.forEach { (category, summaries) -> roomSummaries?.forEach { (category, summaries) ->
if (summaries.isEmpty()) { if (summaries.isEmpty()) {
return@forEach return@forEach
} else { } else {
val isExpanded = viewState.isCategoryExpanded(category) val isExpanded = nonNullViewState.isCategoryExpanded(category)
buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) { buildRoomCategory(nonNullViewState, summaries, category.titleRes, nonNullViewState.isCategoryExpanded(category)) {
listener?.onToggleRoomCategory(category) listener?.onToggleRoomCategory(category)
} }
if (isExpanded) { if (isExpanded) {
buildRoomModels(summaries, buildRoomModels(summaries,
viewState.joiningRoomsIds, nonNullViewState.joiningRoomsIds,
viewState.joiningErrorRoomsIds, nonNullViewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds, nonNullViewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds) nonNullViewState.rejectingErrorRoomsIds)
} }
} }
} }
@ -66,10 +82,10 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
.filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) } .filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
buildRoomModels(filteredSummaries, buildRoomModels(filteredSummaries,
viewState.joiningRoomsIds, viewState.joiningRoomsIds,
viewState.joiningErrorRoomsIds, viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds, viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds) viewState.rejectingErrorRoomsIds)
addFilterFooter(viewState) addFilterFooter(viewState)
} }
@ -105,7 +121,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
showHighlighted(showHighlighted) showHighlighted(showHighlighted)
listener { listener {
mutateExpandedState() mutateExpandedState()
setData(viewState) update(viewState)
} }
} }
} }

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size
android:width="8dp"
android:height="8dp" />
<solid android:color="#00000000" />
</shape>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -9,139 +9,135 @@
<im.vector.riotx.core.platform.CheckableView <im.vector.riotx.core.platform.CheckableView
android:id="@+id/messageSelectedBackground" android:id="@+id/messageSelectedBackground"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:background="?riotx_highlighted_message_background" android:layout_alignBottom="@+id/readMarkerView"
app:layout_constraintBottom_toBottomOf="parent" android:layout_alignParentTop="true"
app:layout_constraintEnd_toEndOf="parent" android:background="?riotx_highlighted_message_background" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView <ImageView
android:id="@+id/messageAvatarImageView" android:id="@+id/messageAvatarImageView"
android:layout_width="44dp" android:layout_width="44dp"
android:layout_height="44dp" android:layout_height="44dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/messageStartGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:layout_constraintGuide_begin="52dp" />
<TextView <TextView
android:id="@+id/messageMemberNameView" android:id="@+id/messageMemberNameView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:layout_marginRight="8dp" android:layout_marginRight="8dp"
android:layout_toStartOf="@+id/messageTimeView"
android:layout_toEndOf="@+id/messageStartGuideline"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="?riotx_text_primary" android:textColor="?riotx_text_primary"
android:textSize="15sp" android:textSize="15sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@+id/messageTimeView"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toEndOf="@id/messageStartGuideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/displayName" /> tools:text="@sample/matrix.json/data/displayName" />
<TextView <TextView
android:id="@+id/messageTimeView" android:id="@+id/messageTimeView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/messageMemberNameView"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginRight="8dp" android:layout_marginRight="8dp"
android:textColor="?riotx_text_secondary" android:textColor="?riotx_text_secondary"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintBaseline_toBaselineOf="@id/messageMemberNameView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/messageMemberNameView"
tools:text="@tools:sample/date/hhmm" /> tools:text="@tools:sample/date/hhmm" />
<View
<ViewStub android:id="@+id/messageStartGuideline"
android:id="@+id/messageContentTextStub"
style="@style/TimelineContentStubLayoutParams"
android:inflatedId="@id/messageTextView"
android:layout="@layout/item_timeline_event_text_message_stub"
tools:ignore="MissingConstraints"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentMediaStub"
style="@style/TimelineContentStubLayoutParams"
android:inflatedId="@+id/messageContentMedia"
android:layout="@layout/item_timeline_event_media_message_stub"
tools:ignore="MissingConstraints" />
<ViewStub
android:id="@+id/messageContentFileStub"
style="@style/TimelineContentStubLayoutParams"
android:layout="@layout/item_timeline_event_file_stub"
tools:ignore="MissingConstraints" />
<ViewStub
android:id="@+id/messageContentRedactedStub"
style="@style/TimelineContentStubLayoutParams"
android:layout_height="20dp"
android:layout_marginEnd="56dp"
android:layout_marginRight="56dp"
android:layout="@layout/item_timeline_event_redacted_stub"
tools:ignore="MissingConstraints" />
<!-- TODO: For now we show 8 reactions maximum, this will need rework when needed-->
<ViewStub
android:id="@+id/messageBottomInfo"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_marginBottom="4dp" tools:layout_marginStart="52dp" />
android:inflatedId="@+id/messageBottomInfo"
android:layout="@layout/item_timeline_event_bottom_reactions_stub"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/readReceiptsView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/messageStartGuideline"
app:layout_constraintVertical_chainStyle="packed"
tools:visibility="visible">
</ViewStub> <FrameLayout
android:id="@+id/viewStubContainer"
<im.vector.riotx.core.ui.views.ReadReceiptsView android:layout_width="match_parent"
android:id="@+id/readReceiptsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_below="@id/messageMemberNameView"
android:layout_marginBottom="4dp" android:layout_toEndOf="@id/messageStartGuideline"
app:layout_constraintBottom_toTopOf="@+id/readMarkerView" android:addStatesFromChildren="true">
app:layout_constraintEnd_toEndOf="parent" />
<ViewStub
android:id="@+id/messageContentTextStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
android:inflatedId="@id/messageTextView"
android:layout="@layout/item_timeline_event_text_message_stub"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentMediaStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
android:inflatedId="@+id/messageContentMedia"
android:layout="@layout/item_timeline_event_media_message_stub" />
<ViewStub
android:id="@+id/messageContentFileStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_file_stub" />
<ViewStub
android:id="@+id/messageContentRedactedStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="20dp"
android:layout_marginEnd="56dp"
android:layout="@layout/item_timeline_event_redacted_stub" />
</FrameLayout>
<LinearLayout
android:id="@+id/informationBottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/viewStubContainer"
android:layout_toEndOf="@+id/messageStartGuideline"
android:addStatesFromChildren="true"
android:orientation="vertical">
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/reactionsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="4dp"
app:dividerDrawable="@drawable/reaction_divider"
app:flexWrap="wrap"
app:showDivider="middle" />
<im.vector.riotx.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp" />
</LinearLayout>
<im.vector.riotx.core.ui.views.ReadMarkerView <im.vector.riotx.core.ui.views.ReadMarkerView
android:id="@+id/readMarkerView" android:id="@+id/readMarkerView"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="2dp" android:layout_height="2dp"
android:background="?attr/vctr_unread_marker_line_color" android:layout_below="@+id/informationBottom"
android:layout_marginBottom="2dp"
android:visibility="invisible"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginBottom="2dp"
app:layout_constraintStart_toStartOf="parent" /> android:background="?attr/vctr_unread_marker_line_color"
android:visibility="invisible" />
</androidx.constraintlayout.widget.ConstraintLayout> </RelativeLayout>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -9,70 +8,76 @@
<im.vector.riotx.core.platform.CheckableView <im.vector.riotx.core.platform.CheckableView
android:id="@+id/messageSelectedBackground" android:id="@+id/messageSelectedBackground"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:background="?riotx_highlighted_message_background" android:layout_alignBottom="@+id/informationBottom"
app:layout_constraintBottom_toBottomOf="parent" android:layout_alignParentTop="true"
app:layout_constraintEnd_toEndOf="parent" android:background="?riotx_highlighted_message_background" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline <View
android:id="@+id/messageStartGuideline" android:id="@+id/messageStartGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:layout_constraintGuide_begin="52dp" />
<ViewStub
android:id="@+id/messageContentNoticeStub"
style="@style/TimelineContentStubNoInfoLayoutParams"
android:layout="@layout/item_timeline_event_notice_stub"
tools:ignore="MissingConstraints"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentDefaultStub"
style="@style/TimelineContentStubNoInfoLayoutParams"
android:inflatedId="@+id/stateMessageView"
android:layout="@layout/item_timeline_event_default_stub"
tools:ignore="MissingConstraints" />
<ViewStub
android:id="@+id/messageContentBlankStub"
style="@style/TimelineContentStubNoInfoLayoutParams"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout="@layout/item_timeline_event_blank_stub" android:layout_marginStart="52dp" />
tools:ignore="MissingConstraints" />
<ViewStub <FrameLayout
android:id="@+id/messageContentMergedheaderStub" android:id="@+id/viewStubContainer"
style="@style/TimelineContentStubNoInfoLayoutParams" android:layout_width="match_parent"
android:layout="@layout/item_timeline_event_merged_header_stub"
tools:ignore="MissingConstraints" />
<im.vector.riotx.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_alignParentTop="true"
android:layout_marginBottom="4dp" android:layout_toEndOf="@id/messageStartGuideline">
app:layout_constraintBottom_toTopOf="@+id/readMarkerView"
app:layout_constraintEnd_toEndOf="parent" />
<im.vector.riotx.core.ui.views.ReadMarkerView <ViewStub
android:id="@+id/readMarkerView" android:id="@+id/messageContentNoticeStub"
android:layout_width="0dp" style="@style/TimelineContentStubBaseParams"
android:layout_height="2dp" android:layout="@layout/item_timeline_event_notice_stub"
android:layout_marginStart="8dp" tools:visibility="visible" />
android:layout_marginEnd="8dp"
android:layout_marginBottom="2dp"
android:background="?attr/vctr_unread_marker_line_color"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ViewStub
android:id="@+id/messageContentDefaultStub"
style="@style/TimelineContentStubBaseParams"
android:inflatedId="@+id/stateMessageView"
android:layout="@layout/item_timeline_event_default_stub" />
</androidx.constraintlayout.widget.ConstraintLayout> <ViewStub
android:id="@+id/messageContentBlankStub"
style="@style/TimelineContentStubBaseParams"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout="@layout/item_timeline_event_blank_stub" />
<ViewStub
android:id="@+id/messageContentMergedheaderStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_merged_header_stub" />
</FrameLayout>
<LinearLayout
android:id="@+id/informationBottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/viewStubContainer"
android:orientation="vertical">
<im.vector.riotx.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp" />
<im.vector.riotx.core.ui.views.ReadMarkerView
android:id="@+id/readMarkerView"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="2dp"
android:background="?attr/vctr_unread_marker_line_color"
android:visibility="invisible" />
</LinearLayout>
</RelativeLayout>

View File

@ -1,102 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/messageBottomInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<im.vector.riotx.features.reactions.widget.ReactionButton
android:id="@+id/messageBottomReaction1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:emoji="👍"
tools:ignore="MissingConstraints"
tools:reaction_count="3"
tools:visibility="visible" />
<im.vector.riotx.features.reactions.widget.ReactionButton
android:id="@+id/messageBottomReaction2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:emoji="👎"
tools:ignore="MissingConstraints"
tools:reaction_count="10"
tools:visibility="visible" />
<im.vector.riotx.features.reactions.widget.ReactionButton
android:id="@+id/messageBottomReaction3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:emoji="😀"
tools:ignore="MissingConstraints"
tools:visibility="visible" />
<im.vector.riotx.features.reactions.widget.ReactionButton
android:id="@+id/messageBottomReaction4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:emoji="☹️"
tools:ignore="MissingConstraints"
tools:visibility="visible" />
<im.vector.riotx.features.reactions.widget.ReactionButton
android:id="@+id/messageBottomReaction5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:emoji="😱"
tools:ignore="MissingConstraints"
tools:visibility="visible" />
<im.vector.riotx.features.reactions.widget.ReactionButton
android:id="@+id/messageBottomReaction6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:emoji="❌"
tools:ignore="MissingConstraints"
tools:visibility="visible" />
<im.vector.riotx.features.reactions.widget.ReactionButton
android:id="@+id/messageBottomReaction7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:emoji="✔️"
tools:ignore="MissingConstraints"
tools:visibility="visible" />
<im.vector.riotx.features.reactions.widget.ReactionButton
android:id="@+id/messageBottomReaction8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:emoji="♥️"
tools:ignore="MissingConstraints"
tools:visibility="visible" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/reactionsFlowHelper"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="2dp"
app:constraint_referenced_ids="messageBottomReaction1,messageBottomReaction2,messageBottomReaction3,messageBottomReaction4,messageBottomReaction5,messageBottomReaction6,messageBottomReaction7,messageBottomReaction8"
app:flow_horizontalBias="0"
app:flow_horizontalGap="8dp"
app:flow_horizontalStyle="packed"
app:flow_verticalBias="0"
app:flow_verticalGap="4dp"
app:flow_wrapMode="chain"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,61 +1,58 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:orientation="vertical">
<include <RelativeLayout
android:id="@+id/itemMergedAvatarListView" android:layout_width="match_parent"
layout="@layout/vector_message_merge_avatar_list" android:layout_height="wrap_content">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
app:layout_constraintEnd_toStartOf="@+id/itemMergedExpandTextView"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView <include
android:id="@+id/itemMergedExpandTextView" android:id="@+id/itemMergedAvatarListView"
android:layout_width="0dp" layout="@layout/vector_message_merge_avatar_list"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:paddingRight="8dp" android:layout_height="wrap_content"
android:layout_marginTop="2dp" android:layout_marginTop="8dp"
android:paddingLeft="8dp" android:layout_alignParentStart="true"
android:paddingTop="4dp" android:layout_toLeftOf="@+id/itemMergedExpandTextView"
android:paddingBottom="4dp" android:layout_marginEnd="16dp" />
android:text="@string/merged_events_expand"
android:textColor="?attr/colorAccent" <TextView
android:textSize="14sp" android:id="@+id/itemMergedExpandTextView"
android:textStyle="italic" android:layout_width="wrap_content"
app:layout_constraintEnd_toEndOf="parent" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" /> android:layout_marginTop="2dp"
android:paddingLeft="8dp"
android:paddingTop="4dp"
android:paddingRight="8dp"
android:paddingBottom="4dp"
android:layout_alignParentEnd="true"
android:text="@string/merged_events_expand"
android:textColor="?attr/colorAccent"
android:textSize="14sp"
android:textStyle="italic" />
</RelativeLayout>
<View <View
android:id="@+id/itemMergedSeparatorView" android:id="@+id/itemMergedSeparatorView"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:background="?attr/riotx_header_panel_background" android:background="?attr/riotx_header_panel_background"/>
app:layout_constraintEnd_toEndOf="@id/itemMergedExpandTextView"
app:layout_constraintStart_toStartOf="@id/itemMergedAvatarListView"
app:layout_constraintTop_toBottomOf="@id/itemMergedExpandTextView" />
<TextView <TextView
android:id="@+id/itemMergedSummaryTextView" android:id="@+id/itemMergedSummaryTextView"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:textColor="?riotx_text_secondary" android:textColor="?riotx_text_secondary"
android:textIsSelectable="false" android:textIsSelectable="false"
android:textSize="14sp" android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/itemMergedAvatarListView"
app:layout_constraintTop_toBottomOf="@id/itemMergedSeparatorView"
tools:text="3 membership changes" /> tools:text="3 membership changes" />
</androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout>

View File

@ -282,7 +282,7 @@
<style name="TimelineContentStubBaseParams"> <style name="TimelineContentStubBaseParams">
<item name="android:layout_width">0dp</item> <item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginStart">8dp</item> <item name="android:layout_marginStart">8dp</item>
<item name="android:layout_marginLeft">8dp</item> <item name="android:layout_marginLeft">8dp</item>
@ -290,20 +290,6 @@
<item name="android:layout_marginRight">8dp</item> <item name="android:layout_marginRight">8dp</item>
<item name="android:layout_marginBottom">4dp</item> <item name="android:layout_marginBottom">4dp</item>
<item name="android:layout_marginTop">4dp</item> <item name="android:layout_marginTop">4dp</item>
<item name="layout_constraintBottom_toBottomOf">parent</item>
<item name="layout_constraintEnd_toEndOf">parent</item>
<item name="layout_constraintStart_toEndOf">@id/messageStartGuideline</item>
</style>
<style name="TimelineContentStubNoInfoLayoutParams" parent="TimelineContentStubBaseParams">
<item name="layout_constraintTop_toTopOf">parent</item>
<item name="layout_constraintBottom_toTopOf">@id/readReceiptsView</item>
</style>
<style name="TimelineContentStubLayoutParams" parent="TimelineContentStubBaseParams">
<item name="layout_constraintTop_toBottomOf">@id/messageMemberNameView</item>
<item name="layout_constraintBottom_toTopOf">@id/messageBottomInfo</item>
</style> </style>
<style name="VectorLabel"> <style name="VectorLabel">