Merge branch 'develop' into feature/Perf

This commit is contained in:
ganfra 2019-07-02 11:25:39 +02:00
commit 9378d30601
41 changed files with 765 additions and 198 deletions

View File

@ -5,16 +5,28 @@
# Build debug version of the RiotX application, from the develop branch and the features branches # Build debug version of the RiotX application, from the develop branch and the features branches
steps: steps:
- label: "Assemble Debug version" - label: "Assemble GPlay Debug version"
agents: agents:
# We use a medium sized instance instead of the normal small ones because # We use a medium sized instance instead of the normal small ones because
# gradle build is long # gradle build is long
queue: "medium" queue: "medium"
commands: commands:
- "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace" - "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace"
- "./gradlew lintFdroidRelease assembleFdroidDebug --stacktrace"
artifact_paths: artifact_paths:
- "vector/build/outputs/apk/gplay/debug/*.apk" - "vector/build/outputs/apk/gplay/debug/*.apk"
branches: "develop feature/*"
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
- label: "Assemble FDroid Debug version"
agents:
# We use a medium sized instance instead of the normal small ones because
# gradle build is long
queue: "medium"
commands:
- "./gradlew clean lintFdroidRelease assembleFdroidDebug --stacktrace"
artifact_paths:
- "vector/build/outputs/apk/fdroid/debug/*.apk" - "vector/build/outputs/apk/fdroid/debug/*.apk"
branches: "develop feature/*" branches: "develop feature/*"
plugins: plugins:

View File

@ -81,13 +81,11 @@ interface Session :
/** /**
* This method start the sync thread. * This method start the sync thread.
*/ */
@MainThread
fun startSync() fun startSync()
/** /**
* This method stop the sync thread. * This method stop the sync thread.
*/ */
@MainThread
fun stopSync() fun stopSync()
/** /**
@ -99,7 +97,6 @@ interface Session :
/** /**
* This method allow to close a session. It does stop some services. * This method allow to close a session. It does stop some services.
*/ */
@MainThread
fun close() fun close()
/** /**

View File

@ -177,11 +177,7 @@ data class Event(
* @return The curve25519 key that sent this event. * @return The curve25519 key that sent this event.
*/ */
fun getSenderKey(): String? { fun getSenderKey(): String? {
return if (null != mClearEvent) { return mClearEvent?.mSenderCurve25519Key ?: mSenderCurve25519Key
mClearEvent!!.mSenderCurve25519Key
} else {
mSenderCurve25519Key
}
} }
/** /**

View File

@ -47,7 +47,7 @@ interface MembershipService {
*/ */
fun getRoomMemberIdsLive(): LiveData<List<String>> fun getRoomMemberIdsLive(): LiveData<List<String>>
fun getNumberOfJoinedMembers() : Int fun getNumberOfJoinedMembers(): Int
/** /**
* Invite a user in the room * Invite a user in the room
@ -55,13 +55,12 @@ interface MembershipService {
fun invite(userId: String, callback: MatrixCallback<Unit>) fun invite(userId: String, callback: MatrixCallback<Unit>)
/** /**
* Join the room * Join the room, or accept an invitation.
*/ */
fun join(callback: MatrixCallback<Unit>) fun join(callback: MatrixCallback<Unit>)
/** /**
* Leave the room. * Leave the room, or reject an invitation.
*
*/ */
fun leave(callback: MatrixCallback<Unit>) fun leave(callback: MatrixCallback<Unit>)

View File

@ -16,8 +16,8 @@
package im.vector.matrix.android.api.session.room.model package im.vector.matrix.android.api.session.room.model
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.tag.RoomTag import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
/** /**
* This class holds some data of a room. * This class holds some data of a room.
@ -29,7 +29,7 @@ data class RoomSummary(
val topic: String = "", val topic: String = "",
val avatarUrl: String = "", val avatarUrl: String = "",
val isDirect: Boolean = false, val isDirect: Boolean = false,
val lastMessage: Event? = null, val latestEvent: TimelineEvent? = null,
val otherMemberIds: List<String> = emptyList(), val otherMemberIds: List<String> = emptyList(),
val notificationCount: Int = 0, val notificationCount: Int = 0,
val highlightCount: Int = 0, val highlightCount: Int = 0,

View File

@ -16,24 +16,37 @@
package im.vector.matrix.android.internal.database.mapper package im.vector.matrix.android.internal.database.mapper
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.model.RoomSummary 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.api.session.room.model.tag.RoomTag
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
import javax.inject.Inject
internal object RoomSummaryMapper { internal class RoomSummaryMapper @Inject constructor(
private val timelineEventFactory: TimelineEventFactory,
private val monarchy: Monarchy) {
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
val tags = roomSummaryEntity.tags.map { val tags = roomSummaryEntity.tags.map {
RoomTag(it.tagName, it.tagOrder) RoomTag(it.tagName, it.tagOrder)
} }
val latestEvent = roomSummaryEntity.latestEvent?.let {
var ev: TimelineEvent? = null
monarchy.doWithRealm { realm ->
ev = timelineEventFactory.create(it, realm)
}
ev
}
return RoomSummary( return RoomSummary(
roomId = roomSummaryEntity.roomId, roomId = roomSummaryEntity.roomId,
displayName = roomSummaryEntity.displayName ?: "", displayName = roomSummaryEntity.displayName ?: "",
topic = roomSummaryEntity.topic ?: "", topic = roomSummaryEntity.topic ?: "",
avatarUrl = roomSummaryEntity.avatarUrl ?: "", avatarUrl = roomSummaryEntity.avatarUrl ?: "",
isDirect = roomSummaryEntity.isDirect, isDirect = roomSummaryEntity.isDirect,
lastMessage = roomSummaryEntity.lastMessage?.asDomain(), latestEvent = latestEvent,
otherMemberIds = roomSummaryEntity.otherMemberIds.toList(), otherMemberIds = roomSummaryEntity.otherMemberIds.toList(),
highlightCount = roomSummaryEntity.highlightCount, highlightCount = roomSummaryEntity.highlightCount,
notificationCount = roomSummaryEntity.notificationCount, notificationCount = roomSummaryEntity.notificationCount,
@ -42,7 +55,3 @@ internal object RoomSummaryMapper {
) )
} }
} }
internal fun RoomSummaryEntity.asDomain(): RoomSummary {
return RoomSummaryMapper.map(this)
}

View File

@ -27,7 +27,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var displayName: String? = "", var displayName: String? = "",
var avatarUrl: String? = "", var avatarUrl: String? = "",
var topic: String? = "", var topic: String? = "",
var lastMessage: EventEntity? = null, var latestEvent: EventEntity? = null,
var heroes: RealmList<String> = RealmList(), var heroes: RealmList<String> = RealmList(),
var joinedMembersCount: Int? = 0, var joinedMembersCount: Int? = 0,
var invitedMembersCount: Int? = 0, var invitedMembersCount: Int? = 0,

View File

@ -16,10 +16,12 @@
package im.vector.matrix.android.internal.database.query package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.internal.database.helper.addSendingEvent
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.* import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomEntity
import io.realm.Realm import io.realm.Realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmQuery import io.realm.RealmQuery
@ -65,7 +67,14 @@ internal fun EventEntity.Companion.latestEvent(realm: Realm,
roomId: String, roomId: String,
includedTypes: List<String> = emptyList(), includedTypes: List<String> = emptyList(),
excludedTypes: List<String> = emptyList()): EventEntity? { excludedTypes: List<String> = emptyList()): EventEntity? {
val query = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.events?.where()
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
val eventList = if (roomEntity.sendingTimelineEvents.isNotEmpty()) {
roomEntity.sendingTimelineEvents
} else {
ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.events
}
val query = eventList?.where()
if (includedTypes.isNotEmpty()) { if (includedTypes.isNotEmpty()) {
query?.`in`(EventEntityFields.TYPE, includedTypes.toTypedArray()) query?.`in`(EventEntityFields.TYPE, includedTypes.toTypedArray())
} else if (excludedTypes.isNotEmpty()) { } else if (excludedTypes.isNotEmpty()) {

View File

@ -20,6 +20,7 @@ import android.content.Context
import android.os.Looper import android.os.Looper
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
@ -102,7 +103,6 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
SyncWorker.stopAnyBackgroundSync(context) SyncWorker.stopAnyBackgroundSync(context)
} }
@MainThread
override fun startSync() { override fun startSync() {
assert(isOpen) assert(isOpen)
if (!syncThread.isAlive) { if (!syncThread.isAlive) {
@ -113,16 +113,14 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
} }
} }
@MainThread
override fun stopSync() { override fun stopSync() {
assert(isOpen) assert(isOpen)
syncThread.kill() syncThread.kill()
} }
@MainThread
override fun close() { override fun close() {
assertMainThread()
assert(isOpen) assert(isOpen)
stopSync()
liveEntityObservers.forEach { it.dispose() } liveEntityObservers.forEach { it.dispose() }
cryptoService.close() cryptoService.close()
if (monarchy.isMonarchyThreadOpen) { if (monarchy.isMonarchyThreadOpen) {
@ -153,6 +151,11 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: clear cache -> SUCCESS: clear crypto cache") Timber.w("SIGN_OUT: clear cache -> SUCCESS: clear crypto cache")
cryptoService.clearCryptoCache(MatrixCallbackDelegate(callback)) cryptoService.clearCryptoCache(MatrixCallbackDelegate(callback))
WorkManager.getInstance(context).also {
it.cancelAllWork()
it.pruneWork()
}
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {

View File

@ -29,7 +29,7 @@ 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.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.asDomain 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
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
@ -38,6 +38,7 @@ import javax.inject.Inject
internal class DefaultRoom @Inject constructor(override val roomId: String, internal class DefaultRoom @Inject constructor(override val roomId: String,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val roomSummaryMapper: RoomSummaryMapper,
private val timelineService: TimelineService, private val timelineService: TimelineService,
private val sendService: SendService, private val sendService: SendService,
private val stateService: StateService, private val stateService: StateService,
@ -58,7 +59,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
} }
Transformations.map(liveRealmData) { results -> Transformations.map(liveRealmData) { results ->
val roomSummaries = results.map { it.asDomain() } val roomSummaries = results.map { roomSummaryMapper.map(it) }
if (roomSummaries.isEmpty()) { if (roomSummaries.isEmpty()) {
// Create a dummy RoomSummary to avoid Crash during Sign Out or clear cache // Create a dummy RoomSummary to avoid Crash during Sign Out or clear cache
@ -72,7 +73,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
override val roomSummary: RoomSummary? override val roomSummary: RoomSummary?
get() { get() {
var sum: RoomSummaryEntity? = monarchy.fetchCopied { RoomSummaryEntity.where(it, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME).findFirst() } var sum: RoomSummaryEntity? = monarchy.fetchCopied { RoomSummaryEntity.where(it, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME).findFirst() }
return sum?.asDomain() return sum?.let { roomSummaryMapper.map(it) }
} }
override fun isEncrypted(): Boolean { override fun isEncrypted(): Boolean {

View File

@ -23,12 +23,11 @@ import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
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
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
@ -36,6 +35,7 @@ import im.vector.matrix.android.internal.util.fetchManaged
import javax.inject.Inject import javax.inject.Inject
internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy, internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy,
private val roomSummaryMapper: RoomSummaryMapper,
private val createRoomTask: CreateRoomTask, private val createRoomTask: CreateRoomTask,
private val roomFactory: RoomFactory, private val roomFactory: RoomFactory,
private val taskExecutor: TaskExecutor) : RoomService { private val taskExecutor: TaskExecutor) : RoomService {
@ -55,7 +55,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> { override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ realm -> RoomSummaryEntity.where(realm).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) }, { realm -> RoomSummaryEntity.where(realm).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) },
{ it.asDomain() } { roomSummaryMapper.map(it) }
) )
} }
} }

View File

@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor
@ -38,8 +39,8 @@ import im.vector.matrix.android.internal.session.room.state.DefaultStateService
import im.vector.matrix.android.internal.session.room.state.SendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.InMemoryTimelineEventFactory
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import javax.inject.Inject import javax.inject.Inject
@ -47,6 +48,7 @@ internal class RoomFactory @Inject constructor(private val context: Context,
private val credentials: Credentials, private val credentials: Credentials,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val eventFactory: LocalEchoEventFactory, private val eventFactory: LocalEchoEventFactory,
private val roomSummaryMapper: RoomSummaryMapper,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val inviteTask: InviteTask, private val inviteTask: InviteTask,
@ -61,9 +63,7 @@ internal class RoomFactory @Inject constructor(private val context: Context,
private val leaveRoomTask: LeaveRoomTask) { private val leaveRoomTask: LeaveRoomTask) {
fun create(roomId: String): Room { fun create(roomId: String): Room {
val roomMemberExtractor = SenderRoomMemberExtractor(roomId) val timelineEventFactory = InMemoryTimelineEventFactory(SenderRoomMemberExtractor(), EventRelationExtractor(), cryptoService)
val relationExtractor = EventRelationExtractor()
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, relationExtractor, cryptoService)
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineEventFactory, contextOfEventTask, paginationTask) val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineEventFactory, contextOfEventTask, paginationTask)
val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy) val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy)
val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask) val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask)
@ -74,6 +74,7 @@ internal class RoomFactory @Inject constructor(private val context: Context,
return DefaultRoom( return DefaultRoom(
roomId, roomId,
monarchy, monarchy,
roomSummaryMapper,
timelineService, timelineService,
sendService, sendService,
stateService, stateService,

View File

@ -138,4 +138,10 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindTimelineService(timelineService: DefaultTimelineService): TimelineService abstract fun bindTimelineService(timelineService: DefaultTimelineService): TimelineService
@Binds
abstract fun bindSimpleTimelineEventFactory(timelineEventFactory: SimpleTimelineEventFactory): TimelineEventFactory
@Binds
abstract fun bindCacheableTimelineEventFactory(inMemoryTimelineEventFactory: InMemoryTimelineEventFactory): CacheableTimelineEventFactory
} }

View File

@ -27,7 +27,6 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
@ -40,6 +39,23 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
private val roomDisplayNameResolver: RoomDisplayNameResolver, private val roomDisplayNameResolver: RoomDisplayNameResolver,
private val roomAvatarResolver: RoomAvatarResolver) { private val roomAvatarResolver: RoomAvatarResolver) {
// TODO: maybe allow user of SDK to give that list
private val PREVIEWABLE_TYPES = listOf(
EventType.MESSAGE,
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_HISTORY_VISIBILITY,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER,
EventType.ENCRYPTED,
EventType.ENCRYPTION,
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER,
EventType.STATE_ROOM_CREATE
)
fun update(realm: Realm, fun update(realm: Realm,
roomId: String, roomId: String,
membership: Membership? = null, membership: Membership? = null,
@ -47,7 +63,7 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
unreadNotifications: RoomSyncUnreadNotifications? = null) { unreadNotifications: RoomSyncUnreadNotifications? = null) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId) ?: realm.createObject(roomId)
if (roomSummary != null) { if (roomSummary != null) {
if (roomSummary.heroes.isNotEmpty()) { if (roomSummary.heroes.isNotEmpty()) {
@ -71,13 +87,13 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
roomSummaryEntity.membership = membership roomSummaryEntity.membership = membership
} }
val lastEvent = EventEntity.latestEvent(realm, roomId) val lastEvent = EventEntity.latestEvent(realm, roomId, includedTypes = PREVIEWABLE_TYPES)
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain() val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain()
val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId } val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId }
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummaryEntity.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic roomSummaryEntity.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
roomSummaryEntity.lastMessage = lastEvent roomSummaryEntity.latestEvent = lastEvent
roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers.keys) roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers.keys)
} }

View File

@ -34,9 +34,10 @@ import io.realm.RealmList
import io.realm.RealmQuery import io.realm.RealmQuery
import javax.inject.Inject import javax.inject.Inject
internal class SenderRoomMemberExtractor @Inject constructor(private val roomId: String) { internal class SenderRoomMemberExtractor @Inject constructor() {
fun extractFrom(event: EventEntity, realm: Realm = event.realm): RoomMember? { fun extractFrom(event: EventEntity, realm: Realm = event.realm): RoomMember? {
val roomId = event.roomId
val sender = event.sender ?: return null val sender = event.sender ?: return null
// If the event is unlinked we want to fetch unlinked state events // If the event is unlinked we want to fetch unlinked state events
val unlinked = event.isUnlinked val unlinked = event.isUnlinked

View File

@ -34,6 +34,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.util.StringProvider import im.vector.matrix.android.internal.util.StringProvider
import im.vector.matrix.android.internal.util.tryTransactionAsync import im.vector.matrix.android.internal.util.tryTransactionAsync
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
@ -50,8 +51,9 @@ import javax.inject.Inject
* *
* The transactionID is used as loc * The transactionID is used as loc
*/ */
internal class LocalEchoEventFactory @Inject constructor(private val credentials: Credentials,
internal class LocalEchoEventFactory @Inject constructor(private val credentials: Credentials, private val stringProvider: StringProvider) { private val stringProvider: StringProvider,
private val roomSummaryUpdater: RoomSummaryUpdater) {
fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event { fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event {
if (autoMarkdown && msgType == MessageType.MSGTYPE_TEXT) { if (autoMarkdown && msgType == MessageType.MSGTYPE_TEXT) {
@ -342,10 +344,12 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
} }
fun saveLocalEcho(monarchy: Monarchy, event: Event) { fun saveLocalEcho(monarchy: Monarchy, event: Event) {
if (event.roomId == null) throw IllegalStateException("Your event should have a roomId")
monarchy.tryTransactionAsync { realm -> monarchy.tryTransactionAsync { realm ->
val roomEntity = RoomEntity.where(realm, roomId = event.roomId!!).findFirst() val roomEntity = RoomEntity.where(realm, roomId = event.roomId).findFirst()
?: return@tryTransactionAsync ?: return@tryTransactionAsync
roomEntity.addSendingEvent(event) roomEntity.addSendingEvent(event)
roomSummaryUpdater.update(realm, event.roomId)
} }
} }

View File

@ -54,7 +54,7 @@ internal class DefaultTimeline(
private val realmConfiguration: RealmConfiguration, private val realmConfiguration: RealmConfiguration,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask, private val contextOfEventTask: GetContextOfEventTask,
private val timelineEventFactory: TimelineEventFactory, private val timelineEventFactory: CacheableTimelineEventFactory,
private val paginationTask: PaginationTask, private val paginationTask: PaginationTask,
private val allowedTypes: List<String>? private val allowedTypes: List<String>?
) : Timeline { ) : Timeline {
@ -129,7 +129,7 @@ internal class DefaultTimeline(
builtEventsIdMap[eventId]?.let { builtIndex -> builtEventsIdMap[eventId]?.let { builtIndex ->
//Update the relation of existing event //Update the relation of existing event
builtEvents[builtIndex]?.let { te -> builtEvents[builtIndex]?.let { te ->
builtEvents[builtIndex] = timelineEventFactory.create(eventEntity) builtEvents[builtIndex] = timelineEventFactory.create(eventEntity, eventEntity.realm)
hasChanged = true hasChanged = true
} }
} }
@ -290,7 +290,7 @@ internal class DefaultTimeline(
roomEntity?.sendingTimelineEvents roomEntity?.sendingTimelineEvents
?.filter { allowedTypes?.contains(it.type) ?: false } ?.filter { allowedTypes?.contains(it.type) ?: false }
?.forEach { ?.forEach {
val timelineEvent = timelineEventFactory.create(it) val timelineEvent = timelineEventFactory.create(it, it.realm)
sendingEvents.add(timelineEvent) sendingEvents.add(timelineEvent)
} }
} }
@ -418,7 +418,7 @@ internal class DefaultTimeline(
nextDisplayIndex = offsetIndex + 1 nextDisplayIndex = offsetIndex + 1
} }
offsetResults.forEach { eventEntity -> offsetResults.forEach { eventEntity ->
val timelineEvent = timelineEventFactory.create(eventEntity) val timelineEvent = timelineEventFactory.create(eventEntity, eventEntity.realm)
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
builtEvents.add(position, timelineEvent) builtEvents.add(position, timelineEvent)
//Need to shift :/ //Need to shift :/

View File

@ -33,7 +33,7 @@ import javax.inject.Inject
internal class DefaultTimelineService @Inject constructor(private val roomId: String, internal class DefaultTimelineService @Inject constructor(private val roomId: String,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val timelineEventFactory: TimelineEventFactory, private val timelineEventFactory: CacheableTimelineEventFactory,
private val contextOfEventTask: GetContextOfEventTask, private val contextOfEventTask: GetContextOfEventTask,
private val paginationTask: PaginationTask private val paginationTask: PaginationTask
) : TimelineService { ) : TimelineService {
@ -60,14 +60,14 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
} }
val result = MediatorLiveData<TimelineEvent>() val result = MediatorLiveData<TimelineEvent>()
result.addSource(liveEventEntity) { realmResults -> result.addSource(liveEventEntity) { realmResults ->
result.value = realmResults.firstOrNull()?.let { timelineEventFactory.create(it) } result.value = realmResults.firstOrNull()?.let { timelineEventFactory.create(it, it.realm) }
} }
result.addSource(liveAnnotationsEntity) { result.addSource(liveAnnotationsEntity) {
liveEventEntity.value?.let { liveEventEntity.value?.let {
result.value = liveEventEntity.value?.let { realmResults -> result.value = liveEventEntity.value?.let { realmResults ->
//recreate the timeline event //recreate the timeline event
realmResults.firstOrNull()?.let { timelineEventFactory.create(it) } realmResults.firstOrNull()?.let { timelineEventFactory.create(it, it.realm) }
} }
} }
} }

View File

@ -33,20 +33,69 @@ import timber.log.Timber
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
internal interface TimelineEventFactory {
fun create(eventEntity: EventEntity, realm: Realm): TimelineEvent
}
internal interface CacheableTimelineEventFactory : TimelineEventFactory {
fun clear()
}
/** /**
* This class is responsible for building [TimelineEvent] returned by a [Timeline] through [TimelineService] * This class is responsible for building [TimelineEvent] returned by a [Timeline] through [TimelineService]
* It handles decryption, extracting additional data around an event as sender data and relation. * It handles decryption, extracting additional data around an event as sender data and relation.
*/ */
internal class TimelineEventFactory @Inject constructor( internal class SimpleTimelineEventFactory @Inject constructor(private val roomMemberExtractor: SenderRoomMemberExtractor,
private val roomMemberExtractor: SenderRoomMemberExtractor, private val relationExtractor: EventRelationExtractor,
private val relationExtractor: EventRelationExtractor, private val cryptoService: CryptoService
private val cryptoService: CryptoService) { ) : TimelineEventFactory {
override fun create(eventEntity: EventEntity, realm: Realm): TimelineEvent {
val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity, realm)
val relations = relationExtractor.extractFrom(eventEntity, realm)
val event = eventEntity.asDomain()
if (event.getClearType() == EventType.ENCRYPTED) {
handleEncryptedEvent(event)
}
val isUniqueDisplayName = RoomMembers(realm, eventEntity.roomId).isUniqueDisplayName(senderRoomMember?.displayName)
return TimelineEvent(
event,
eventEntity.localId,
eventEntity.displayIndex,
senderRoomMember?.displayName,
isUniqueDisplayName,
senderRoomMember?.avatarUrl,
eventEntity.sendState,
relations
)
}
private fun handleEncryptedEvent(event: Event) {
Timber.v("Encrypted event: try to decrypt ${event.eventId}")
try {
val result = cryptoService.decryptEvent(event, UUID.randomUUID().toString())
event.setClearData(result)
} catch (failure: Throwable) {
Timber.e(failure, "Encrypted event: decryption failed")
if (failure is MXDecryptionException) {
event.setCryptoError(failure.cryptoError)
}
}
}
}
internal class InMemoryTimelineEventFactory @Inject constructor(private val roomMemberExtractor: SenderRoomMemberExtractor,
private val relationExtractor: EventRelationExtractor,
private val cryptoService: CryptoService) : CacheableTimelineEventFactory {
private val timelineId = UUID.randomUUID().toString() private val timelineId = UUID.randomUUID().toString()
private val senderCache = mutableMapOf<String, SenderData>() private val senderCache = mutableMapOf<String, SenderData>()
private val decryptionCache = mutableMapOf<String, MXEventDecryptionResult>() private val decryptionCache = mutableMapOf<String, MXEventDecryptionResult>()
fun create(eventEntity: EventEntity, realm: Realm = eventEntity.realm): TimelineEvent { override fun create(eventEntity: EventEntity, realm: Realm): TimelineEvent {
val sender = eventEntity.sender val sender = eventEntity.sender
val cacheKey = sender + eventEntity.localId val cacheKey = sender + eventEntity.localId
val senderData = senderCache.getOrPut(cacheKey) { val senderData = senderCache.getOrPut(cacheKey) {
@ -97,8 +146,9 @@ internal class TimelineEventFactory @Inject constructor(
} }
} }
fun clear() { override fun clear() {
senderCache.clear() senderCache.clear()
decryptionCache.clear()
} }
private data class SenderData( private data class SenderData(

View File

@ -29,7 +29,7 @@ import im.vector.riotredesign.features.themes.ThemeUtils
/** /**
* Set a text in the TextView, or set visibility to GONE if the text is null * Set a text in the TextView, or set visibility to GONE if the text is null
*/ */
fun TextView.setTextOrHide(newText: String?, hideWhenBlank: Boolean = true) { fun TextView.setTextOrHide(newText: CharSequence?, hideWhenBlank: Boolean = true) {
if (newText == null if (newText == null
|| (newText.isBlank() && hideWhenBlank)) { || (newText.isBlank() && hideWhenBlank)) {
isVisible = false isVisible = false

View File

@ -19,12 +19,15 @@ package im.vector.riotredesign.features.autocomplete.user
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.features.autocomplete.AutocompleteClickListener import im.vector.riotredesign.features.autocomplete.AutocompleteClickListener
import im.vector.riotredesign.features.home.AvatarRenderer
import javax.inject.Inject import javax.inject.Inject
class AutocompleteUserController @Inject constructor(): TypedEpoxyController<List<User>>() { class AutocompleteUserController @Inject constructor(): TypedEpoxyController<List<User>>() {
var listener: AutocompleteClickListener<User>? = null var listener: AutocompleteClickListener<User>? = null
@Inject lateinit var avatarRenderer: AvatarRenderer
override fun buildModels(data: List<User>?) { override fun buildModels(data: List<User>?) {
if (data.isNullOrEmpty()) { if (data.isNullOrEmpty()) {
return return
@ -35,6 +38,7 @@ class AutocompleteUserController @Inject constructor(): TypedEpoxyController<Lis
userId(user.userId) userId(user.userId)
name(user.displayName) name(user.displayName)
avatarUrl(user.avatarUrl) avatarUrl(user.avatarUrl)
avatarRenderer(avatarRenderer)
clickListener { _ -> clickListener { _ ->
listener?.onItemClick(user) listener?.onItemClick(user)
} }

View File

@ -81,7 +81,10 @@ import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandP
import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotredesign.features.command.Command import im.vector.riotredesign.features.command.Command
import im.vector.riotredesign.features.home.* import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.NavigateToRoomInterceptor
import im.vector.riotredesign.features.home.PermalinkHandler
import im.vector.riotredesign.features.home.getColorFromUserId
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerActions import im.vector.riotredesign.features.home.room.detail.composer.TextComposerActions
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerView import im.vector.riotredesign.features.home.room.detail.composer.TextComposerView
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
@ -509,6 +512,9 @@ class RoomDetailFragment :
} else if (summary?.membership == Membership.INVITE && inviter != null) { } else if (summary?.membership == Membership.INVITE && inviter != null) {
inviteView.visibility = View.VISIBLE inviteView.visibility = View.VISIBLE
inviteView.render(inviter, VectorInviteView.Mode.LARGE) inviteView.render(inviter, VectorInviteView.Mode.LARGE)
// Intercept click event
inviteView.setOnClickListener { }
} else if (state.asyncInviter.complete) { } else if (state.asyncInviter.complete) {
vectorBaseActivity.finish() vectorBaseActivity.finish()
} }

View File

@ -504,7 +504,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun observeInvitationState() { private fun observeInvitationState() {
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
if (summary.membership == Membership.INVITE) { if (summary.membership == Membership.INVITE) {
summary.lastMessage?.senderId?.let { senderId -> summary.latestEvent?.root?.senderId?.let { senderId ->
session.getUser(senderId) session.getUser(senderId)
}?.also { }?.also {
setState { copy(asyncInviter = Success(it)) } setState { copy(asyncInviter = Success(it)) }

View File

@ -25,14 +25,14 @@ class ChronologicalRoomComparator @Inject constructor() : Comparator<RoomSummary
var rightTimestamp = 0L var rightTimestamp = 0L
var leftTimestamp = 0L var leftTimestamp = 0L
if (null != leftRoomSummary) { if (null != leftRoomSummary) {
leftTimestamp = leftRoomSummary.lastMessage?.originServerTs ?: 0 leftTimestamp = leftRoomSummary.latestEvent?.root?.originServerTs ?: 0
} }
if (null != rightRoomSummary) { if (null != rightRoomSummary) {
rightTimestamp = rightRoomSummary.lastMessage?.originServerTs ?: 0 rightTimestamp = rightRoomSummary.latestEvent?.root?.originServerTs ?: 0
} }
return if (rightRoomSummary?.lastMessage == null) { return if (rightRoomSummary?.latestEvent?.root == null) {
-1 -1
} else if (leftRoomSummary?.lastMessage == null) { } else if (leftRoomSummary?.latestEvent?.root == null) {
1 1
} else { } else {
val deltaTimestamp = rightTimestamp - leftTimestamp val deltaTimestamp = rightTimestamp - leftTimestamp

View File

@ -0,0 +1,105 @@
/*
* 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.riotredesign.features.home.room.list
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.setTextOrHide
import im.vector.riotredesign.core.platform.ButtonStateView
import im.vector.riotredesign.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_room_invitation)
abstract class RoomInvitationItem : VectorEpoxyModel<RoomInvitationItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var roomName: CharSequence
@EpoxyAttribute lateinit var roomId: String
@EpoxyAttribute var secondLine: CharSequence? = null
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var listener: (() -> Unit)? = null
@EpoxyAttribute var invitationAcceptInProgress: Boolean = false
@EpoxyAttribute var invitationAcceptInError: Boolean = false
@EpoxyAttribute var invitationRejectInProgress: Boolean = false
@EpoxyAttribute var invitationRejectInError: Boolean = false
@EpoxyAttribute var acceptListener: (() -> Unit)? = null
@EpoxyAttribute var rejectListener: (() -> Unit)? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.setOnClickListener { listener?.invoke() }
// When a request is in progress (accept or reject), we only use the accept State button
val requestInProgress = invitationAcceptInProgress || invitationRejectInProgress
when {
requestInProgress -> holder.acceptView.render(ButtonStateView.State.Loading)
invitationAcceptInError -> holder.acceptView.render(ButtonStateView.State.Error)
else -> holder.acceptView.render(ButtonStateView.State.Button)
}
// ButtonStateView.State.Loaded not used because roomSummary will not be displayed as a room invitation anymore
holder.acceptView.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
acceptListener?.invoke()
}
override fun onRetryClicked() {
acceptListener?.invoke()
}
}
holder.rejectView.isVisible = !requestInProgress
when {
invitationRejectInError -> holder.rejectView.render(ButtonStateView.State.Error)
else -> holder.rejectView.render(ButtonStateView.State.Button)
}
holder.rejectView.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
rejectListener?.invoke()
}
override fun onRetryClicked() {
rejectListener?.invoke()
}
}
holder.titleView.text = roomName
holder.subtitleView.setTextOrHide(secondLine)
avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
}
class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.roomInvitationNameView)
val subtitleView by bind<TextView>(R.id.roomInvitationSubTitle)
val acceptView by bind<ButtonStateView>(R.id.roomInvitationAccept)
val rejectView by bind<ButtonStateView>(R.id.roomInvitationReject)
val avatarImageView by bind<ImageView>(R.id.roomInvitationAvatarImageView)
val rootView by bind<ViewGroup>(R.id.itemRoomInvitationLayout)
}
}

View File

@ -22,8 +22,10 @@ sealed class RoomListActions {
data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions() data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions()
data class FilterRooms(val roomName: CharSequence? = null) : RoomListActions()
data class ToggleCategory(val category: RoomCategory) : RoomListActions() data class ToggleCategory(val category: RoomCategory) : RoomListActions()
data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListActions()
data class RejectInvitation(val roomSummary: RoomSummary) : RoomListActions()
} }

View File

@ -24,12 +24,15 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.* import com.airbnb.mvrx.*
import com.google.android.material.snackbar.Snackbar
import im.vector.matrix.android.api.failure.Failure 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.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.ScreenComponent import im.vector.riotredesign.core.di.ScreenComponent
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.extensions.observeEventDebounced import im.vector.riotredesign.core.extensions.observeEventDebounced
import im.vector.riotredesign.core.platform.OnBackPressed import im.vector.riotredesign.core.platform.OnBackPressed
import im.vector.riotredesign.core.platform.StateView import im.vector.riotredesign.core.platform.StateView
@ -45,7 +48,7 @@ data class RoomListParams(
) : Parcelable ) : Parcelable
class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, OnBackPressed, FabMenuView.Listener { class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
enum class DisplayMode(@StringRes val titleRes: Int) { enum class DisplayMode(@StringRes val titleRes: Int) {
HOME(R.string.bottom_action_home), HOME(R.string.bottom_action_home),
@ -64,6 +67,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
private val roomListParams: RoomListParams by args() private val roomListParams: RoomListParams by args()
@Inject lateinit var roomController: RoomSummaryController @Inject lateinit var roomController: RoomSummaryController
@Inject lateinit var roomListViewModelFactory: RoomListViewModel.Factory @Inject lateinit var roomListViewModelFactory: RoomListViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
private val roomListViewModel: RoomListViewModel by fragmentViewModel() private val roomListViewModel: RoomListViewModel by fragmentViewModel()
override fun getLayoutResId() = R.layout.fragment_room_list override fun getLayoutResId() = R.layout.fragment_room_list
@ -82,6 +86,13 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
} }
createChatFabMenu.listener = this createChatFabMenu.listener = this
roomListViewModel.invitationAnswerErrorLiveData.observeEvent(this) { throwable ->
vectorBaseActivity.coordinatorLayout?.let {
Snackbar.make(it, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT)
.show()
}
}
} }
private fun setupCreateRoomButton() { private fun setupCreateRoomButton() {
@ -135,7 +146,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
epoxyRecyclerView.layoutManager = layoutManager epoxyRecyclerView.layoutManager = layoutManager
epoxyRecyclerView.itemAnimator = RoomListAnimator() epoxyRecyclerView.itemAnimator = RoomListAnimator()
roomController.callback = this roomController.listener = this
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) } roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
stateView.contentView = epoxyRecyclerView stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(roomController) epoxyRecyclerView.setController(roomController)
@ -233,6 +244,14 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
roomListViewModel.accept(RoomListActions.SelectRoom(room)) roomListViewModel.accept(RoomListActions.SelectRoom(room))
} }
override fun onAcceptRoomInvitation(room: RoomSummary) {
roomListViewModel.accept(RoomListActions.AcceptInvitation(room))
}
override fun onRejectRoomInvitation(room: RoomSummary) {
roomListViewModel.accept(RoomListActions.RejectInvitation(room))
}
override fun onToggleRoomCategory(roomCategory: RoomCategory) { override fun onToggleRoomCategory(roomCategory: RoomCategory) {
roomListViewModel.accept(RoomListActions.ToggleCategory(roomCategory)) roomListViewModel.accept(RoomListActions.ToggleCategory(roomCategory))
} }

View File

@ -18,13 +18,12 @@ package im.vector.riotredesign.features.home.room.list
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import arrow.core.Option
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject 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.Session
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
@ -32,8 +31,7 @@ import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.riotredesign.core.platform.VectorViewModel import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.home.HomeRoomListObservableStore import im.vector.riotredesign.features.home.HomeRoomListObservableStore
import timber.log.Timber
typealias RoomListFilterName = CharSequence
class RoomListViewModel @AssistedInject constructor(@Assisted initialState: RoomListViewState, class RoomListViewModel @AssistedInject constructor(@Assisted initialState: RoomListViewState,
private val session: Session, private val session: Session,
@ -57,22 +55,25 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
} }
private val displayMode = initialState.displayMode private val displayMode = initialState.displayMode
private val roomListDisplayModeFilter = RoomListDisplayModeFilter(displayMode)
private val roomListFilter = BehaviorRelay.createDefault<Option<RoomListFilterName>>(Option.empty())
private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>() private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
val openRoomLiveData: LiveData<LiveEvent<String>> val openRoomLiveData: LiveData<LiveEvent<String>>
get() = _openRoomLiveData get() = _openRoomLiveData
private val _invitationAnswerErrorLiveData = MutableLiveData<LiveEvent<Throwable>>()
val invitationAnswerErrorLiveData: LiveData<LiveEvent<Throwable>>
get() = _invitationAnswerErrorLiveData
init { init {
observeRoomSummaries() observeRoomSummaries()
} }
fun accept(action: RoomListActions) { fun accept(action: RoomListActions) {
when (action) { when (action) {
is RoomListActions.SelectRoom -> handleSelectRoom(action) is RoomListActions.SelectRoom -> handleSelectRoom(action)
is RoomListActions.FilterRooms -> handleFilterRooms(action) is RoomListActions.ToggleCategory -> handleToggleCategory(action)
is RoomListActions.ToggleCategory -> handleToggleCategory(action) is RoomListActions.AcceptInvitation -> handleAcceptInvitation(action)
is RoomListActions.RejectInvitation -> handleRejectInvitation(action)
} }
} }
@ -82,11 +83,6 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
_openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId)) _openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId))
} }
private fun handleFilterRooms(action: RoomListActions.FilterRooms) {
val optionalFilter = Option.fromNullable(action.roomName)
roomListFilter.accept(optionalFilter)
}
private fun handleToggleCategory(action: RoomListActions.ToggleCategory) = setState { private fun handleToggleCategory(action: RoomListActions.ToggleCategory) = setState {
this.toggle(action.category) this.toggle(action.category)
} }
@ -106,6 +102,78 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
} }
} }
private fun handleAcceptInvitation(action: RoomListActions.AcceptInvitation) = withState { state ->
val roomId = action.roomSummary.roomId
if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) {
// Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
setState {
copy(
joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { add(roomId) },
rejectingErrorRoomsIds = rejectingErrorRoomsIds.toMutableSet().apply { remove(roomId) }
)
}
session.getRoom(roomId)?.join(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
// Instead, we wait for the room to be joined
}
override fun onFailure(failure: Throwable) {
// Notify the user
_invitationAnswerErrorLiveData.postValue(LiveEvent(failure))
setState {
copy(
joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { remove(roomId) },
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { add(roomId) }
)
}
}
})
}
private fun handleRejectInvitation(action: RoomListActions.RejectInvitation) = withState { state ->
val roomId = action.roomSummary.roomId
if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) {
// Request already sent, should not happen
Timber.w("Try to reject an already rejecting room. Should not happen")
return@withState
}
setState {
copy(
rejectingRoomsIds = rejectingRoomsIds.toMutableSet().apply { add(roomId) },
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { remove(roomId) }
)
}
session.getRoom(roomId)?.leave(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
// Instead, we wait for the room to be joined
}
override fun onFailure(failure: Throwable) {
// Notify the user
_invitationAnswerErrorLiveData.postValue(LiveEvent(failure))
setState {
copy(
rejectingRoomsIds = rejectingRoomsIds.toMutableSet().apply { remove(roomId) },
rejectingErrorRoomsIds = rejectingErrorRoomsIds.toMutableSet().apply { add(roomId) }
)
}
}
})
}
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries { private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
val invites = ArrayList<RoomSummary>() val invites = ArrayList<RoomSummary>()
val favourites = ArrayList<RoomSummary>() val favourites = ArrayList<RoomSummary>()

View File

@ -27,6 +27,14 @@ data class RoomListViewState(
val displayMode: RoomListFragment.DisplayMode, val displayMode: RoomListFragment.DisplayMode,
val asyncRooms: Async<List<RoomSummary>> = Uninitialized, val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val asyncFilteredRooms: Async<RoomSummaries> = Uninitialized, val asyncFilteredRooms: Async<RoomSummaries> = Uninitialized,
// List of roomIds that the user wants to join
val joiningRoomsIds: Set<String> = emptySet(),
// List of roomIds that the user wants to join, but an error occurred
val joiningErrorRoomsIds: Set<String> = emptySet(),
// List of roomIds that the user wants to join
val rejectingRoomsIds: Set<String> = emptySet(),
// List of roomIds that the user wants to reject, but an error occurred
val rejectingErrorRoomsIds: Set<String> = emptySet(),
val isInviteExpanded: Boolean = true, val isInviteExpanded: Boolean = true,
val isFavouriteRoomsExpanded: Boolean = true, val isFavouriteRoomsExpanded: Boolean = true,
val isDirectRoomsExpanded: Boolean = true, val isDirectRoomsExpanded: Boolean = true,

View File

@ -18,25 +18,15 @@ package im.vector.riotredesign.features.home.room.list
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
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.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.core.resources.DateProvider
import im.vector.riotredesign.core.resources.StringProvider import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
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 eventFormatter: NoticeEventFormatter, private val roomSummaryItemFactory: RoomSummaryItemFactory
private val timelineDateFormatter: TimelineDateFormatter,
private val avatarRenderer: AvatarRenderer
) : TypedEpoxyController<RoomListViewState>() { ) : TypedEpoxyController<RoomListViewState>() {
var callback: Callback? = null var listener: Listener? = null
override fun buildModels(viewState: RoomListViewState) { override fun buildModels(viewState: RoomListViewState) {
val roomSummaries = viewState.asyncFilteredRooms() val roomSummaries = viewState.asyncFilteredRooms()
@ -46,10 +36,14 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
} else { } else {
val isExpanded = viewState.isCategoryExpanded(category) val isExpanded = viewState.isCategoryExpanded(category)
buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) { buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) {
callback?.onToggleRoomCategory(category) listener?.onToggleRoomCategory(category)
} }
if (isExpanded) { if (isExpanded) {
buildRoomModels(summaries) buildRoomModels(summaries,
viewState.joiningRoomsIds,
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds)
} }
} }
} }
@ -83,52 +77,23 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
} }
} }
private fun buildRoomModels(summaries: List<RoomSummary>) { private fun buildRoomModels(summaries: List<RoomSummary>,
joiningRoomsIds: Set<String>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>) {
summaries.forEach { roomSummary -> summaries.forEach { roomSummary ->
val unreadCount = roomSummary.notificationCount roomSummaryItemFactory
val showHighlighted = roomSummary.highlightCount > 0 .create(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
.addTo(this)
var lastMessageFormatted: CharSequence = ""
var lastMessageTime: CharSequence = ""
val lastMessage = roomSummary.lastMessage
if (lastMessage != null) {
val date = lastMessage.localDateTime()
val currentData = DateProvider.currentLocalDateTime()
val isSameDay = date.toLocalDate() == currentData.toLocalDate()
//TODO: get formatted
if (lastMessage.type == EventType.MESSAGE) {
val content = lastMessage.content?.toModel<MessageContent>()
lastMessageFormatted = content?.body ?: ""
} else {
lastMessageFormatted = lastMessage.type
}
lastMessageTime = if (isSameDay) {
timelineDateFormatter.formatMessageHour(date)
} else {
//TODO: change this
timelineDateFormatter.formatMessageDay(date)
}
}
roomSummaryItem {
avatarRenderer(avatarRenderer)
id(roomSummary.roomId)
roomId(roomSummary.roomId)
lastEventTime(lastMessageTime)
lastFormattedEvent(lastMessageFormatted)
roomName(roomSummary.displayName)
avatarUrl(roomSummary.avatarUrl)
showHighlighted(showHighlighted)
unreadCount(unreadCount)
listener { callback?.onRoomSelected(roomSummary) }
}
} }
} }
interface Callback { interface Listener {
fun onToggleRoomCategory(roomCategory: RoomCategory) fun onToggleRoomCategory(roomCategory: RoomCategory)
fun onRoomSelected(room: RoomSummary) fun onRoomSelected(room: RoomSummary)
fun onRejectRoomInvitation(room: RoomSummary)
fun onAcceptRoomInvitation(room: RoomSummary)
} }
} }

View File

@ -0,0 +1,136 @@
/*
* 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.riotredesign.features.home.room.list
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.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.core.resources.ColorProvider
import im.vector.riotredesign.core.resources.DateProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
import me.gujun.android.span.span
import javax.inject.Inject
class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatter: NoticeEventFormatter,
private val timelineDateFormatter: TimelineDateFormatter,
private val colorProvider: ColorProvider,
private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer) {
fun create(roomSummary: RoomSummary,
joiningRoomsIds: Set<String>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
return when (roomSummary.membership) {
Membership.INVITE -> createInvitationItem(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
else -> createRoomItem(roomSummary, listener)
}
}
private fun createInvitationItem(roomSummary: RoomSummary,
joiningRoomsIds: Set<String>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
val secondLine = if (roomSummary.isDirect) {
roomSummary.latestEvent?.root?.senderId
} else {
roomSummary.latestEvent?.root?.senderId?.let {
stringProvider.getString(R.string.invited_by, it)
}
}
return RoomInvitationItem_()
.id(roomSummary.roomId)
.avatarRenderer(avatarRenderer)
.roomId(roomSummary.roomId)
.secondLine(secondLine)
.invitationAcceptInProgress(joiningRoomsIds.contains(roomSummary.roomId))
.invitationAcceptInError(joiningErrorRoomsIds.contains(roomSummary.roomId))
.invitationRejectInProgress(rejectingRoomsIds.contains(roomSummary.roomId))
.invitationRejectInError(rejectingErrorRoomsIds.contains(roomSummary.roomId))
.acceptListener { listener?.onAcceptRoomInvitation(roomSummary) }
.rejectListener { listener?.onRejectRoomInvitation(roomSummary) }
.roomName(roomSummary.displayName)
.avatarUrl(roomSummary.avatarUrl)
.listener { listener?.onRoomSelected(roomSummary) }
}
private fun createRoomItem(roomSummary: RoomSummary, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
val unreadCount = roomSummary.notificationCount
val showHighlighted = roomSummary.highlightCount > 0
var latestFormattedEvent: CharSequence = ""
var latestEventTime: CharSequence = ""
val latestEvent = roomSummary.latestEvent
if (latestEvent != null) {
val date = latestEvent.root.localDateTime()
val currentDate = DateProvider.currentLocalDateTime()
val isSameDay = date.toLocalDate() == currentDate.toLocalDate()
latestFormattedEvent = if (latestEvent.root.getClearType() == EventType.MESSAGE) {
val senderName = latestEvent.senderName() ?: latestEvent.root.senderId
val content = latestEvent.root.getClearContent()?.toModel<MessageContent>()
val message = content?.body ?: ""
if (roomSummary.isDirect.not() && senderName != null) {
span {
text = senderName
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)
}
.append(" - ")
.append(message)
} else {
message
}
} else {
span {
text = noticeEventFormatter.format(latestEvent) ?: ""
textStyle = "italic"
}
}
latestEventTime = if (isSameDay) {
timelineDateFormatter.formatMessageHour(date)
} else {
//TODO: change this
timelineDateFormatter.formatMessageDay(date)
}
}
return RoomSummaryItem_()
.id(roomSummary.roomId)
.avatarRenderer(avatarRenderer)
.roomId(roomSummary.roomId)
.lastEventTime(latestEventTime)
.lastFormattedEvent(latestFormattedEvent)
.roomName(roomSummary.displayName)
.avatarUrl(roomSummary.avatarUrl)
.showHighlighted(showHighlighted)
.unreadCount(unreadCount)
.listener { listener?.onRoomSelected(roomSummary) }
}
}

View File

@ -20,13 +20,13 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.Person import androidx.core.app.Person
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.riotredesign.BuildConfig import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.ActiveSessionHolder import im.vector.riotredesign.core.di.ActiveSessionHolder
import im.vector.riotredesign.core.utils.SecretStoringUtils import im.vector.riotredesign.core.utils.SecretStoringUtils
import im.vector.riotredesign.features.settings.PreferencesManager import im.vector.riotredesign.features.settings.PreferencesManager
import me.gujun.android.span.span
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@ -275,9 +275,32 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
} }
try { try {
val summaryLine = context.resources.getQuantityString( if (events.size == 1) {
R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size) val event = events[0]
summaryInboxStyle.addLine(summaryLine) if (roomEventGroupInfo.isDirect) {
val line = span {
span {
textStyle = "bold"
+String.format("%s: ", event.senderName)
}
+(event.description ?: "")
}
summaryInboxStyle.addLine(line)
} else {
val line = span {
span {
textStyle = "bold"
+String.format("%s: %s ", roomName, event.senderName)
}
+(event.description ?: "")
}
summaryInboxStyle.addLine(line)
}
} else {
val summaryLine = context.resources.getQuantityString(
R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size)
summaryInboxStyle.addLine(summaryLine)
}
} catch (e: Throwable) { } catch (e: Throwable) {
//String not found or bad format //String not found or bad format
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string") Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string")
@ -343,6 +366,11 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
val sumTitle = context.resources.getQuantityString( val sumTitle = context.resources.getQuantityString(
R.plurals.notification_compat_summary_title, nbEvents, nbEvents) R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
summaryInboxStyle.setBigContentTitle(sumTitle) summaryInboxStyle.setBigContentTitle(sumTitle)
//TODO get latest event?
.setSummaryText(
context.resources
.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
NotificationUtils.buildSummaryListNotification( NotificationUtils.buildSummaryListNotification(
context, context,
summaryInboxStyle, summaryInboxStyle,

View File

@ -606,7 +606,7 @@ object NotificationUtils {
* Build the summary notification * Build the summary notification
*/ */
fun buildSummaryListNotification(context: Context, fun buildSummaryListNotification(context: Context,
style: NotificationCompat.Style, style: NotificationCompat.InboxStyle,
compatSummary: String, compatSummary: String,
noisy: Boolean, noisy: Boolean,
lastMessageTimestamp: Long): Notification? { lastMessageTimestamp: Long): Notification? {

View File

@ -203,7 +203,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes
var userId = "undefined" var userId = "undefined"
var olmVersion = "undefined" var olmVersion = "undefined"
activeSessionHolder.getActiveSession().let { session -> activeSessionHolder.getSafeActiveSession()?.let { session ->
userId = session.sessionParams.credentials.userId userId = session.sessionParams.credentials.userId
deviceId = session.sessionParams.credentials.deviceId ?: "undefined" deviceId = session.sessionParams.credentials.deviceId ?: "undefined"
olmVersion = session.getCryptoVersion(context, true) olmVersion = session.getCryptoVersion(context, true)

View File

@ -28,11 +28,11 @@ data class PublicRoomsViewState(
val asyncPublicRoomsRequest: Async<List<PublicRoom>> = Uninitialized, val asyncPublicRoomsRequest: Async<List<PublicRoom>> = Uninitialized,
// True if more result are available server side // True if more result are available server side
val hasMore: Boolean = false, val hasMore: Boolean = false,
// List of roomIds that the user wants to join // Set of roomIds that the user wants to join
val joiningRoomsIds: List<String> = emptyList(), val joiningRoomsIds: Set<String> = emptySet(),
// List of roomIds that the user wants to join, but an error occurred // Set of roomIds that the user wants to join, but an error occurred
val joiningErrorRoomsIds: List<String> = emptyList(), val joiningErrorRoomsIds: Set<String> = emptySet(),
// List of joined roomId, // Set of joined roomId,
val joinedRoomsIds: List<String> = emptyList(), val joinedRoomsIds: Set<String> = emptySet(),
val roomDirectoryDisplayName: String? = null val roomDirectoryDisplayName: String? = null
) : MvRxState ) : MvRxState

View File

@ -18,13 +18,7 @@ package im.vector.riotredesign.features.roomdirectory
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.*
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.airbnb.mvrx.appendAt
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
@ -95,19 +89,19 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
.liveRoomSummaries() .liveRoomSummaries()
.subscribe { list -> .subscribe { list ->
val joinedRoomIds = list val joinedRoomIds = list
// Keep only joined room // Keep only joined room
?.filter { it.membership == Membership.JOIN } ?.filter { it.membership == Membership.JOIN }
?.map { it.roomId } ?.map { it.roomId }
?.toList() ?.toSet()
?: emptyList() ?: emptySet()
setState { setState {
copy( copy(
joinedRoomsIds = joinedRoomIds, joinedRoomsIds = joinedRoomIds,
// Remove (newly) joined room id from the joining room list // Remove (newly) joined room id from the joining room list
joiningRoomsIds = joiningRoomsIds.toMutableList().apply { removeAll(joinedRoomIds) }, joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { removeAll(joinedRoomIds) },
// Remove (newly) joined room id from the joining room list in error // Remove (newly) joined room id from the joining room list in error
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableList().apply { removeAll(joinedRoomIds) } joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { removeAll(joinedRoomIds) }
) )
} }
} }
@ -166,39 +160,39 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
private fun load() { private fun load() {
currentTask = session.getPublicRooms(roomDirectoryData.homeServer, currentTask = session.getPublicRooms(roomDirectoryData.homeServer,
PublicRoomsParams( PublicRoomsParams(
limit = PUBLIC_ROOMS_LIMIT, limit = PUBLIC_ROOMS_LIMIT,
filter = PublicRoomsFilter(searchTerm = currentFilter), filter = PublicRoomsFilter(searchTerm = currentFilter),
includeAllNetworks = roomDirectoryData.includeAllNetworks, includeAllNetworks = roomDirectoryData.includeAllNetworks,
since = since, since = since,
thirdPartyInstanceId = roomDirectoryData.thirdPartyInstanceId thirdPartyInstanceId = roomDirectoryData.thirdPartyInstanceId
), ),
object : MatrixCallback<PublicRoomsResponse> { object : MatrixCallback<PublicRoomsResponse> {
override fun onSuccess(data: PublicRoomsResponse) { override fun onSuccess(data: PublicRoomsResponse) {
currentTask = null currentTask = null
since = data.nextBatch since = data.nextBatch
setState { setState {
copy( copy(
asyncPublicRoomsRequest = Success(data.chunk!!), asyncPublicRoomsRequest = Success(data.chunk!!),
// It's ok to append at the end of the list, so I use publicRooms.size() // It's ok to append at the end of the list, so I use publicRooms.size()
publicRooms = publicRooms.appendAt(data.chunk!!, publicRooms.size), publicRooms = publicRooms.appendAt(data.chunk!!, publicRooms.size),
hasMore = since != null hasMore = since != null
) )
} }
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
currentTask = null currentTask = null
setState { setState {
copy( copy(
asyncPublicRoomsRequest = Fail(failure) asyncPublicRoomsRequest = Fail(failure)
) )
} }
} }
}) })
} }
fun joinRoom(publicRoom: PublicRoom) = withState { state -> fun joinRoom(publicRoom: PublicRoom) = withState { state ->
@ -210,7 +204,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
setState { setState {
copy( copy(
joiningRoomsIds = joiningRoomsIds.toMutableList().apply { add(publicRoom.roomId) } joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { add(publicRoom.roomId) }
) )
} }
@ -226,8 +220,8 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
setState { setState {
copy( copy(
joiningRoomsIds = joiningRoomsIds.toMutableList().apply { remove(publicRoom.roomId) }, joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { remove(publicRoom.roomId) },
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableList().apply { add(publicRoom.roomId) } joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { add(publicRoom.roomId) }
) )
} }
} }

View File

@ -7,7 +7,7 @@
tools:openDrawer="start"> tools:openDrawer="start">
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout" android:id="@+id/vector_coordinator_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">

View File

@ -93,6 +93,7 @@
android:id="@+id/inviteView" android:id="@+id/inviteView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?riotx_background"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -0,0 +1,117 @@
<?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/itemRoomInvitationLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<ImageView
android:id="@+id/roomInvitationAvatarImageView"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<!-- Margin bottom does not work, so I use space -->
<Space
android:id="@+id/roomInvitationAvatarBottomSpace"
android:layout_width="0dp"
android:layout_height="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/roomInvitationAvatarImageView"
tools:layout_marginStart="20dp" />
<TextView
android:id="@+id/roomInvitationNameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginLeft="@dimen/layout_horizontal_margin"
android:layout_marginTop="12dp"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
android:drawableEnd="@drawable/ic_arrow_right"
android:drawablePadding="8dp"
android:duplicateParentState="true"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/roomInvitationAvatarImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/displayName" />
<TextView
android:id="@+id/roomInvitationSubTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="@+id/roomInvitationNameView"
app:layout_constraintStart_toStartOf="@+id/roomInvitationNameView"
app:layout_constraintTop_toBottomOf="@+id/roomInvitationNameView"
tools:text="@sample/matrix.json/data/message" />
<!-- Margin bottom does not work, so I use space -->
<Space
android:id="@+id/roomLastEventBottomSpace"
android:layout_width="0dp"
android:layout_height="7dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/roomInvitationSubTitle"
tools:layout_marginStart="120dp" />
<im.vector.riotredesign.core.platform.ButtonStateView
android:id="@+id/roomInvitationAccept"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:minWidth="122dp"
app:bsv_button_text="@string/accept"
app:bsv_loaded_image_src="@drawable/ic_tick"
app:bsv_use_flat_button="false"
app:layout_constraintEnd_toEndOf="@+id/roomInvitationNameView"
app:layout_constraintTop_toBottomOf="@+id/roomLastEventBottomSpace" />
<im.vector.riotredesign.core.platform.ButtonStateView
android:id="@+id/roomInvitationReject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/layout_vertical_margin"
android:minWidth="122dp"
app:bsv_button_text="@string/reject"
app:bsv_loaded_image_src="@drawable/ic_tick"
app:bsv_use_flat_button="true"
app:layout_constraintEnd_toStartOf="@+id/roomInvitationAccept"
app:layout_constraintTop_toTopOf="@+id/roomInvitationAccept" />
<View
android:id="@+id/roomInvitationDividerView"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="5dp"
android:background="?riotx_header_panel_border_mobile"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/roomInvitationAccept" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name" translatable="false">"Riot X"</string> <string name="app_name" translatable="false">"RiotX"</string>
<!-- server urls --> <!-- server urls -->
<string name="vector_im_server_url" translatable="false">https://vector.im</string> <string name="vector_im_server_url" translatable="false">https://vector.im</string>

View File

@ -142,6 +142,16 @@
<item name="colorControlHighlight">?colorAccent</item> <item name="colorControlHighlight">?colorAccent</item>
</style> </style>
<style name="VectorButtonStyleOutlined" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="android:textStyle">bold</item>
<item name="android:textAllCaps">false</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:background">@null</item>
<!--item name="android:textColor">?colorAccent</item-->
<item name="colorControlHighlight">?colorAccent</item>
</style>
<style name="AlerterButton" parent="Widget.AppCompat.Button.Borderless.Colored"> <style name="AlerterButton" parent="Widget.AppCompat.Button.Borderless.Colored">
<item name="colorAccent">@android:color/white</item> <item name="colorAccent">@android:color/white</item>
<item name="android:textColor">@android:color/white</item> <item name="android:textColor">@android:color/white</item>