First attempt to handle room name + make app listen to room summaries instead of rooms

This commit is contained in:
ganfra 2018-10-23 18:25:28 +02:00
parent 279241974a
commit f747d268c9
40 changed files with 568 additions and 144 deletions

View File

@ -3,6 +3,7 @@ package im.vector.riotredesign
import android.app.Application
import im.vector.matrix.android.BuildConfig
import im.vector.riotredesign.core.di.AppModule
import org.koin.log.EmptyLogger
import org.koin.standalone.StandAloneContext.startKoin
import timber.log.Timber

@ -13,7 +14,7 @@ class Riot : Application() {
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
startKoin(listOf(AppModule(this)))
startKoin(listOf(AppModule(this)), logger = EmptyLogger())
}

}

View File

@ -5,11 +5,11 @@ import im.vector.matrix.android.api.session.events.model.EnrichedEvent

class EventDiffUtilCallback : DiffUtil.ItemCallback<EnrichedEvent>() {
override fun areItemsTheSame(p0: EnrichedEvent, p1: EnrichedEvent): Boolean {
return p0.core.eventId == p1.core.eventId
return p0.root.eventId == p1.root.eventId
}

override fun areContentsTheSame(p0: EnrichedEvent, p1: EnrichedEvent): Boolean {
return p0.core == p1.core
return p0.root == p1.root
&& p0.getMetaEvents()
.zip(p1.getMetaEvents()) { a, b ->
a.eventId == b.eventId

View File

@ -1,20 +0,0 @@
package im.vector.riotredesign.features.home

import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.room.Room

class RoomController(private val callback: Callback? = null) : TypedEpoxyController<List<Room>>() {

override fun buildModels(data: List<Room>?) {
data?.forEach {
RoomItem(it.roomId, listener = { callback?.onRoomSelected(it) })
.id(it.roomId)
.addTo(this)
}
}

interface Callback {
fun onRoomSelected(room: Room)
}

}

View File

@ -55,9 +55,10 @@ class RoomDetailFragment : RiotFragment(), TimelineEventAdapter.Callback {
layoutManager.stackFromEnd = true
timelineAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (layoutManager.findFirstVisibleItemPosition() == 0) {
/*if (layoutManager.findFirstVisibleItemPosition() == 0) {
layoutManager.scrollToPosition(0)
}
*/
}
})
recyclerView.layoutManager = layoutManager

View File

@ -5,7 +5,7 @@ import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel

data class RoomItem(
val title: String,
val title: CharSequence,
val listener: (() -> Unit)? = null
) : KotlinModel(R.layout.item_room) {


View File

@ -2,19 +2,18 @@ package im.vector.riotredesign.features.home

import android.arch.lifecycle.Observer
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
import im.vector.riotredesign.core.platform.RiotFragment
import kotlinx.android.synthetic.main.fragment_room_list.*
import org.koin.android.ext.android.inject

class RoomListFragment : RiotFragment(), RoomController.Callback {
class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {

companion object {

@ -26,7 +25,7 @@ class RoomListFragment : RiotFragment(), RoomController.Callback {

private val matrix by inject<Matrix>()
private val currentSession = matrix.currentSession!!
private val roomController = RoomController(this)
private lateinit var roomController: RoomSummaryController

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_room_list, container, false)
@ -34,15 +33,16 @@ class RoomListFragment : RiotFragment(), RoomController.Callback {

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
roomController = RoomSummaryController(this)
epoxyRecyclerView.setController(roomController)
currentSession.liveRooms().observe(this, Observer<List<Room>> { renderRooms(it) })
currentSession.liveRoomSummaries().observe(this, Observer<List<RoomSummary>> { renderRooms(it) })
}

private fun renderRooms(rooms: List<Room>?) {
private fun renderRooms(rooms: List<RoomSummary>?) {
roomController.setData(rooms)
}

override fun onRoomSelected(room: Room) {
override fun onRoomSelected(room: RoomSummary) {
val detailFragment = RoomDetailFragment.newInstance(room.roomId)
addFragmentToBackstack(detailFragment, R.id.homeFragmentContainer)
}

View File

@ -0,0 +1,21 @@
package im.vector.riotredesign.features.home

import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.room.model.RoomSummary

class RoomSummaryController(private val callback: Callback? = null
) : TypedEpoxyController<List<RoomSummary>>() {

override fun buildModels(data: List<RoomSummary>?) {
data?.forEach {
RoomItem(it.displayName, listener = { callback?.onRoomSelected(it) })
.id(it.roomId)
.addTo(this)
}
}

interface Callback {
fun onRoomSelected(room: RoomSummary)
}

}

View File

@ -38,10 +38,10 @@ class TimelineEventAdapter(private val callback: Callback? = null)
val titleView = view.findViewById<TextView>(R.id.titleView)!!

fun bind(event: EnrichedEvent?) {
if (event == null || event.core.type != EventType.MESSAGE) {
if (event == null) {
titleView.text = null
} else {
val messageContent = event.core.content<MessageContent>()
} else if (event.root.type == EventType.MESSAGE) {
val messageContent = event.root.content<MessageContent>()
val roomMember = event.getMetaEvents(EventType.STATE_ROOM_MEMBER).firstOrNull()?.content<RoomMember>()
if (messageContent == null || roomMember == null) {
titleView.text = null
@ -49,6 +49,8 @@ class TimelineEventAdapter(private val callback: Callback? = null)
val text = "${roomMember.displayName} : ${messageContent.body}"
titleView.text = text
}
} else {
titleView.text = event.root.toString()
}
}
}

View File

@ -2,11 +2,12 @@ package im.vector.matrix.android.api.session

import android.support.annotation.MainThread
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.internal.database.SessionRealmHolder
import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.auth.data.SessionParams

interface Session : RoomService {

val sessionParams: SessionParams

@MainThread
fun open()


View File

@ -1,6 +1,6 @@
package im.vector.matrix.android.api.session.events.model

data class EnrichedEvent(val core: Event) {
data class EnrichedEvent(val root: Event) {

private val metaEventsByType = HashMap<String, ArrayList<Event>>()


View File

@ -3,11 +3,16 @@ package im.vector.matrix.android.api.session.room
import android.arch.lifecycle.LiveData
import android.arch.paging.PagedList
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.room.model.MyMembership

interface Room {

val roomId: String

val myMembership: MyMembership

fun liveTimeline(): LiveData<PagedList<EnrichedEvent>>

fun getNumberOfJoinedMembers(): Int

}

View File

@ -1,6 +1,7 @@
package im.vector.matrix.android.api.session.room

import android.arch.lifecycle.LiveData
import im.vector.matrix.android.api.session.room.model.RoomSummary

interface RoomService {

@ -10,4 +11,7 @@ interface RoomService {

fun liveRooms(): LiveData<List<Room>>

fun liveRoomSummaries(): LiveData<List<RoomSummary>>


}

View File

@ -0,0 +1,8 @@
package im.vector.matrix.android.api.session.room.model

enum class MyMembership {
JOINED,
LEFT,
INVITED,
NONE
}

View File

@ -0,0 +1,9 @@
package im.vector.matrix.android.api.session.room.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class RoomAliasesContent(
@Json(name = "aliases") val aliases: List<String> = emptyList()
)

View File

@ -0,0 +1,9 @@
package im.vector.matrix.android.api.session.room.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class RoomCanonicalAliasContent(
@Json(name = "alias") val canonicalAlias: String? = null
)

View File

@ -12,4 +12,4 @@ data class RoomMember(
@Json(name = "is_direct") val isDirect: Boolean = false,
@Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null,
@Json(name = "unsigned") val unsignedData: UnsignedData? = null
)
)

View File

@ -5,5 +5,5 @@ import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class RoomNameContent(
@Json(name = "name") val name: String
@Json(name = "name") val name: String? = null
)

View File

@ -0,0 +1,7 @@
package im.vector.matrix.android.api.session.room.model

data class RoomSummary(
val roomId: String,
var displayName: String = "",
var topic: String = ""
)

View File

@ -0,0 +1,21 @@
package im.vector.matrix.android.internal.database.mapper

import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.session.room.DefaultRoom


object RoomMapper {


internal fun map(roomEntity: RoomEntity): Room {
return DefaultRoom(
roomEntity.roomId,
roomEntity.membership
)
}
}

fun RoomEntity.asDomain(): Room {
return RoomMapper.map(this)
}

View File

@ -0,0 +1,20 @@
package im.vector.matrix.android.internal.database.mapper

import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity


object RoomSummaryMapper {

internal fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
return RoomSummary(
roomSummaryEntity.roomId,
roomSummaryEntity.displayName ?: "",
roomSummaryEntity.topic ?: ""
)
}
}

fun RoomSummaryEntity.asDomain(): RoomSummary {
return RoomSummaryMapper.map(this)
}

View File

@ -1,5 +1,6 @@
package im.vector.matrix.android.internal.database.model

import im.vector.matrix.android.api.session.room.model.MyMembership
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.Ignore
@ -10,19 +11,12 @@ open class RoomEntity(@PrimaryKey var roomId: String = "",
var chunks: RealmList<ChunkEntity> = RealmList()
) : RealmObject() {

private var membershipStr: String = Membership.NONE.name
private var membershipStr: String = MyMembership.NONE.name

@delegate:Ignore var membership: Membership by Delegates.observable(Membership.valueOf(membershipStr)) { _, _, newValue ->
@delegate:Ignore var membership: MyMembership by Delegates.observable(MyMembership.valueOf(membershipStr)) { _, _, newValue ->
membershipStr = newValue.name
}

companion object;

enum class Membership {
JOINED,
LEFT,
INVITED,
NONE
}
}


View File

@ -1,5 +1,6 @@
package im.vector.matrix.android.internal.database.model

import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey

@ -7,5 +8,12 @@ import io.realm.annotations.PrimaryKey
open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var displayName: String? = "",
var topic: String? = "",
var lastMessage: EventEntity? = null
) : RealmObject()
var lastMessage: EventEntity? = null,
var heroes: RealmList<String> = RealmList(),
var joinedMembersCount: Int? = 0,
var invitedMembersCount: Int? = 0
) : RealmObject() {

companion object

}

View File

@ -1,26 +1,43 @@
package im.vector.matrix.android.internal.database.query

import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.Sort

fun EventEntity.Companion.where(realm: Realm, roomId: String, type: String? = null): RealmQuery<EventEntity> {
var query = realm.where(EventEntity::class.java)
val query = realm.where(EventEntity::class.java)
.equalTo("chunk.room.roomId", roomId)
if (type != null) {
query = query.equalTo("type", type)
query.equalTo("type", type)
}
return query
}

fun EventEntity.Companion.stateEvents(realm: Realm, roomId: String): RealmQuery<EventEntity> {
return realm.where(EventEntity::class.java)
.equalTo("chunk.room.roomId", roomId)
.isNotNull("stateKey")
}

fun RealmQuery<EventEntity>.last(from: Long? = null): EventEntity? {
var query = this
if (from != null) {
query = query.lessThanOrEqualTo("originServerTs", from)
this.lessThanOrEqualTo("originServerTs", from)
}
return query
return this
.sort("originServerTs", Sort.DESCENDING)
.findFirst()
}

fun EventEntity.Companion.findAllRoomMembers(realm: Realm, roomId: String): Map<String, RoomMember> {
return EventEntity
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
.sort("originServerTs")
.findAll()
.map { it.asDomain() }
.associateBy { it.stateKey!! }
.mapValues { it.value.content<RoomMember>()!! }
}

View File

@ -1,5 +1,6 @@
package im.vector.matrix.android.internal.database.query

import im.vector.matrix.android.api.session.room.model.MyMembership
import im.vector.matrix.android.internal.database.model.RoomEntity
import io.realm.Realm
import io.realm.RealmQuery
@ -8,7 +9,7 @@ fun RoomEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<RoomEnt
return realm.where<RoomEntity>(RoomEntity::class.java).equalTo("roomId", roomId)
}

fun RoomEntity.Companion.where(realm: Realm, membership: RoomEntity.Membership? = null): RealmQuery<RoomEntity> {
fun RoomEntity.Companion.where(realm: Realm, membership: MyMembership? = null): RealmQuery<RoomEntity> {
val query = realm.where(RoomEntity::class.java)
if (membership != null) {
query.equalTo("membership", membership.name)

View File

@ -0,0 +1,13 @@
package im.vector.matrix.android.internal.database.query

import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import io.realm.Realm
import io.realm.RealmQuery

fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<RoomSummaryEntity> {
val query = realm.where(RoomSummaryEntity::class.java)
if (roomId != null) {
query.equalTo("roomId", roomId)
}
return query
}

View File

@ -25,7 +25,9 @@ class NetworkModule : Module {

single {
val logger = HttpLoggingInterceptor.Logger { message -> Timber.v(message) }
HttpLoggingInterceptor(logger).apply { level = HttpLoggingInterceptor.Level.BASIC }
val interceptor = HttpLoggingInterceptor(logger)
interceptor.level = HttpLoggingInterceptor.Level.BASIC
interceptor
}

single {

View File

@ -6,9 +6,10 @@ import android.support.annotation.MainThread
import im.vector.matrix.android.api.session.Session
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.model.RoomSummary
import im.vector.matrix.android.internal.auth.data.SessionParams
import im.vector.matrix.android.internal.session.room.RoomModule
import im.vector.matrix.android.internal.session.room.RoomSummaryObserver
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.sync.SyncModule
import im.vector.matrix.android.internal.session.sync.job.SyncThread
import org.koin.core.scope.Scope
@ -18,7 +19,7 @@ import org.koin.standalone.getKoin
import org.koin.standalone.inject


class DefaultSession(private val sessionParams: SessionParams) : Session, KoinComponent, RoomService {
class DefaultSession(override val sessionParams: SessionParams) : Session, KoinComponent, RoomService {

companion object {
const val SCOPE: String = "session"
@ -26,7 +27,7 @@ class DefaultSession(private val sessionParams: SessionParams) : Session, KoinCo

private lateinit var scope: Scope

private val roomSummaryObserver by inject<RoomSummaryObserver>()
private val roomSummaryObserver by inject<RoomSummaryUpdater>()
private val roomService by inject<RoomService>()
private val syncThread by inject<SyncThread>()
private var isOpen = false
@ -70,6 +71,10 @@ class DefaultSession(private val sessionParams: SessionParams) : Session, KoinCo
return roomService.liveRooms()
}

override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return roomService.liveRoomSummaries()
}

// Private methods *****************************************************************************

private fun checkIsMainThread() {

View File

@ -7,7 +7,9 @@ import im.vector.matrix.android.internal.legacy.MXDataHandler
import im.vector.matrix.android.internal.legacy.MXSession
import im.vector.matrix.android.internal.legacy.data.store.MXFileStore
import im.vector.matrix.android.internal.session.room.DefaultRoomService
import im.vector.matrix.android.internal.session.room.RoomSummaryObserver
import im.vector.matrix.android.internal.session.room.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.RoomMemberDisplayNameResolver
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import io.realm.RealmConfiguration
import org.koin.dsl.context.ModuleDefinition
import org.koin.dsl.module.Module
@ -34,7 +36,15 @@ class SessionModule(private val sessionParams: SessionParams) : Module {
}

scope(DefaultSession.SCOPE) {
RoomSummaryObserver(get())
RoomMemberDisplayNameResolver()
}

scope(DefaultSession.SCOPE) {
RoomDisplayNameResolver(get(), get(), sessionParams)
}

scope(DefaultSession.SCOPE) {
RoomSummaryUpdater(get(), get(), get())
}

scope(DefaultSession.SCOPE) {

View File

@ -1,6 +1,7 @@
package im.vector.matrix.android.api.session.events.interceptor
package im.vector.matrix.android.internal.session.events.interceptor

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.interceptor.EnrichedEventInterceptor
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.mapper.asDomain
@ -11,15 +12,15 @@ import im.vector.matrix.android.internal.database.query.where
class MessageEventInterceptor(val monarchy: Monarchy) : EnrichedEventInterceptor {

override fun canEnrich(event: EnrichedEvent): Boolean {
return event.core.type == EventType.MESSAGE
return event.root.type == EventType.MESSAGE
}

override fun enrich(roomId: String, event: EnrichedEvent) {
monarchy.doWithRealm { realm ->
val roomMember = EventEntity
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
.equalTo("stateKey", event.core.sender)
.last(from = event.core.originServerTs)
.equalTo("stateKey", event.root.sender)
.last(from = event.root.originServerTs)
?.asDomain()
event.enrichWith(roomMember)
}

View File

@ -5,12 +5,14 @@ import android.arch.paging.LivePagedListBuilder
import android.arch.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.interceptor.EnrichedEventInterceptor
import im.vector.matrix.android.api.session.events.interceptor.MessageEventInterceptor
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.MyMembership
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.events.interceptor.MessageEventInterceptor
import im.vector.matrix.android.internal.session.room.timeline.PaginationRequest
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
import io.realm.Sort
@ -19,7 +21,8 @@ import org.koin.standalone.inject
import java.util.concurrent.Executors

data class DefaultRoom(
override val roomId: String
override val roomId: String,
override val myMembership: MyMembership
) : Room, KoinComponent {

private val paginationRequest by inject<PaginationRequest>()
@ -63,4 +66,10 @@ data class DefaultRoom(
return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
}

override fun getNumberOfJoinedMembers(): Int {
val roomSummary = monarchy.fetchAllCopiedSync { realm -> RoomSummaryEntity.where(realm, roomId) }.firstOrNull()
return roomSummary?.joinedMembersCount ?: 0
}


}

View File

@ -4,7 +4,10 @@ import android.arch.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
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.model.RoomSummary
import im.vector.matrix.android.internal.database.mapper.asDomain
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.query.where

class DefaultRoomService(private val monarchy: Monarchy) : RoomService {
@ -12,7 +15,7 @@ class DefaultRoomService(private val monarchy: Monarchy) : RoomService {
override fun getAllRooms(): List<Room> {
var rooms: List<Room> = emptyList()
monarchy.doWithRealm { realm ->
rooms = RoomEntity.where(realm).findAll().map { DefaultRoom(it.roomId) }
rooms = RoomEntity.where(realm).findAll().map { it.asDomain() }
}
return rooms
}
@ -20,7 +23,7 @@ class DefaultRoomService(private val monarchy: Monarchy) : RoomService {
override fun getRoom(roomId: String): Room? {
var room: Room? = null
monarchy.doWithRealm { realm ->
room = RoomEntity.where(realm, roomId).findFirst()?.let { DefaultRoom(it.roomId) }
room = RoomEntity.where(realm, roomId).findFirst()?.let { it.asDomain() }
}
return room
}
@ -28,8 +31,16 @@ class DefaultRoomService(private val monarchy: Monarchy) : RoomService {
override fun liveRooms(): LiveData<List<Room>> {
return monarchy.findAllMappedWithChanges(
{ realm -> RoomEntity.where(realm) },
{ DefaultRoom(it.roomId) }
{ it.asDomain() }
)
}

override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges(
{ realm -> RoomSummaryEntity.where(realm) },
{ it.asDomain() }
)
}


}

View File

@ -0,0 +1,130 @@
/*
* Copyright 2018 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.session.room

import android.content.Context
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.R
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.MyMembership
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.internal.auth.data.SessionParams
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.findAllRoomMembers
import im.vector.matrix.android.internal.database.query.last
import im.vector.matrix.android.internal.database.query.where

/**
* This class computes room display name
*/
class RoomDisplayNameResolver(private val monarchy: Monarchy,
private val roomMemberDisplayNameResolver: RoomMemberDisplayNameResolver,
private val sessionParams: SessionParams
) {

/**
* Compute the room display name
*
* @param context
* @param room: the room to resolve the name of.
* @return the room display name
*/
fun resolve(context: Context, room: Room): CharSequence {
// this algorithm is the one defined in
// https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617
// calculateRoomName(room, userId)

// For Lazy Loaded room, see algorithm here:
// https://docs.google.com/document/d/11i14UI1cUz-OJ0knD5BFu7fmT6Fo327zvMYqfSAR7xs/edit#heading=h.qif6pkqyjgzn
var name: CharSequence? = null
monarchy.doWithRealm { realm ->
val roomName = EventEntity.where(realm, room.roomId, EventType.STATE_ROOM_NAME).last()?.asDomain()
name = roomName?.content<RoomNameContent>()?.name
if (!name.isNullOrEmpty()) {
return@doWithRealm
}

val canonicalAlias = EventEntity.where(realm, room.roomId, EventType.STATE_CANONICAL_ALIAS).last()?.asDomain()
name = canonicalAlias?.content<RoomCanonicalAliasContent>()?.canonicalAlias
if (!name.isNullOrEmpty()) {
return@doWithRealm
}

val aliases = EventEntity.where(realm, room.roomId, EventType.STATE_ROOM_ALIASES).last()?.asDomain()
name = aliases?.content<RoomAliasesContent>()?.aliases?.firstOrNull()
if (!name.isNullOrEmpty()) {
return@doWithRealm
}

val otherRoomMembers = EventEntity
.findAllRoomMembers(realm, room.roomId)
.filterKeys { it != sessionParams.credentials.userId }

if (room.myMembership == MyMembership.INVITED) {
//TODO handle invited
/*
if (currentUser != null
&& !othersActiveMembers.isEmpty()
&& !TextUtils.isEmpty(currentUser!!.mSender)) {
// extract who invited us to the room
name = context.getString(R.string.room_displayname_invite_from, roomState.resolve(currentUser!!.mSender))
} else {
name = context.getString(R.string.room_displayname_room_invite)
}
*/
name = context.getString(R.string.room_displayname_room_invite)
} else {

val roomSummary = RoomSummaryEntity.where(realm, room.roomId).findFirst()
val memberIds = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes
} else {
otherRoomMembers.keys.toList()
}

val nbOfOtherMembers = memberIds.size

when (nbOfOtherMembers) {
0 -> name = context.getString(R.string.room_displayname_empty_room)
1 -> name = roomMemberDisplayNameResolver.resolve(memberIds[0], otherRoomMembers)
2 -> {
val member1 = memberIds[0]
val member2 = memberIds[1]
name = context.getString(R.string.room_displayname_two_members,
roomMemberDisplayNameResolver.resolve(member1, otherRoomMembers),
roomMemberDisplayNameResolver.resolve(member2, otherRoomMembers)
)
}
else -> {
val member = memberIds[0]
name = context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members,
room.getNumberOfJoinedMembers() - 1,
roomMemberDisplayNameResolver.resolve(member, otherRoomMembers),
room.getNumberOfJoinedMembers() - 1)
}
}
}
return@doWithRealm
}
return name ?: room.roomId
}
}

View File

@ -0,0 +1,37 @@
package im.vector.matrix.android.internal.session.room

import im.vector.matrix.android.api.session.room.model.RoomMember

class RoomMemberDisplayNameResolver {

fun resolve(userId: String, members: Map<String, RoomMember>): String? {
var displayName: String? = null
val currentMember = members[userId]
// Get the user display name from the member list of the room
// Do not consider null display name
if (currentMember != null && !currentMember.displayName.isNullOrEmpty()) {
val hasNameCollision = members
.filterValues { it != currentMember && it.displayName == currentMember.displayName }
.isNotEmpty()
displayName = if (hasNameCollision) {
"${currentMember.displayName} ( $userId )"
} else {
currentMember.displayName
}
}
// TODO handle invited users
/*else if (null != member && TextUtils.equals(member!!.membership, RoomMember.MEMBERSHIP_INVITE)) {
val user = (mDataHandler as MXDataHandler).getUser(userId)
if (null != user) {
displayName = user!!.displayname
}
}
*/
if (displayName == null) {
// By default, use the user ID
displayName = userId
}
return displayName
}

}

View File

@ -1,63 +0,0 @@
package im.vector.matrix.android.internal.session.room

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
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.query.last
import im.vector.matrix.android.internal.database.query.where
import io.realm.RealmResults
import java.util.concurrent.atomic.AtomicBoolean

internal class RoomSummaryObserver(private val monarchy: Monarchy) {

private lateinit var roomResults: RealmResults<RoomEntity>
private var isStarted = AtomicBoolean(false)

fun start() {
if (isStarted.compareAndSet(false, true)) {
monarchy.doWithRealm {
roomResults = RoomEntity.where(it).findAllAsync()
roomResults.addChangeListener { rooms, changeSet ->
manageRoomResults(rooms, changeSet.changes)
manageRoomResults(rooms, changeSet.insertions)
}
}
}
}

fun dispose() {
if (isStarted.compareAndSet(true, false)) {
roomResults.removeAllChangeListeners()
}
}

// PRIVATE

private fun manageRoomResults(rooms: RealmResults<RoomEntity>, indexes: IntArray) {
indexes.forEach {
val room = rooms[it]
if (room != null) {
manageRoom(room.roomId)
}
}
}

private fun manageRoom(roomId: String) {
monarchy.writeAsync { realm ->
val lastNameEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).last()?.asDomain()
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).last()?.asDomain()
val lastMessageEvent = EventEntity.where(realm, roomId, EventType.MESSAGE).last()

val roomSummary = realm.copyToRealmOrUpdate(RoomSummaryEntity(roomId))
roomSummary.displayName = lastNameEvent?.content<RoomNameContent>()?.name
roomSummary.topic = lastTopicEvent?.content<RoomTopicContent>()?.topic
roomSummary.lastMessage = lastMessageEvent
}
}

}

View File

@ -0,0 +1,80 @@
package im.vector.matrix.android.internal.session.room

import android.arch.lifecycle.Observer
import android.content.Context
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
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.query.last
import im.vector.matrix.android.internal.database.query.where
import io.realm.RealmResults
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean

internal class RoomSummaryUpdater(private val monarchy: Monarchy,
private val roomDisplayNameResolver: RoomDisplayNameResolver,
private val context: Context
) : Observer<Monarchy.ManagedChangeSet<RoomEntity>> {

private var isStarted = AtomicBoolean(false)
private val liveResults = monarchy.findAllManagedWithChanges { RoomEntity.where(it) }

fun start() {
if (isStarted.compareAndSet(false, true)) {
liveResults.observeForever(this)
}
}

fun dispose() {
if (isStarted.compareAndSet(true, false)) {
liveResults.removeObserver(this)
}
}

// PRIVATE

override fun onChanged(changeSet: Monarchy.ManagedChangeSet<RoomEntity>?) {
if (changeSet == null) {
return
}
manageRoomResults(changeSet.realmResults, changeSet.orderedCollectionChangeSet.changes)
manageRoomResults(changeSet.realmResults, changeSet.orderedCollectionChangeSet.insertions)
}


private fun manageRoomResults(rooms: RealmResults<RoomEntity>, indexes: IntArray) {
indexes.forEach {
val room = rooms[it]?.asDomain()
try {
manageRoom(room)
} catch (e: Exception) {
Timber.e(e, "An error occured when updating room summaries")
}
}
}

private fun manageRoom(room: Room?) {
if (room == null) {
return
}


monarchy.writeAsync { realm ->
val roomSummary = RoomSummaryEntity.where(realm, room.roomId).findFirst()
?: RoomSummaryEntity(room.roomId)

val lastMessageEvent = EventEntity.where(realm, room.roomId, EventType.MESSAGE).last()
val lastTopicEvent = EventEntity.where(realm, room.roomId, EventType.STATE_ROOM_TOPIC).last()?.asDomain()

roomSummary.displayName = roomDisplayNameResolver.resolve(context, room).toString()
roomSummary.topic = lastTopicEvent?.content<RoomTopicContent>()?.topic
roomSummary.lastMessage = lastMessageEvent
}
}

}

View File

@ -28,10 +28,10 @@ class TimelineBoundaryCallback(private val paginationRequest: PaginationRequest,
override fun onItemAtEndLoaded(itemAtEnd: EnrichedEvent) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
monarchy.doWithRealm { realm ->
if (itemAtEnd.core.eventId == null) {
if (itemAtEnd.root.eventId == null) {
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtEnd.core.eventId)).firstOrNull()
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtEnd.root.eventId)).firstOrNull()
paginationRequest.execute(roomId, chunkEntity?.prevToken, PaginationDirection.BACKWARDS, callback = createCallback(it))
}
}
@ -40,10 +40,10 @@ class TimelineBoundaryCallback(private val paginationRequest: PaginationRequest,
override fun onItemAtFrontLoaded(itemAtFront: EnrichedEvent) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) {
monarchy.doWithRealm { realm ->
if (itemAtFront.core.eventId == null) {
if (itemAtFront.root.eventId == null) {
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtFront.core.eventId)).firstOrNull()
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtFront.root.eventId)).firstOrNull()
paginationRequest.execute(roomId, chunkEntity?.nextToken, PaginationDirection.FORWARDS, callback = createCallback(it))
}
}

View File

@ -2,14 +2,17 @@ package im.vector.matrix.android.internal.session.sync

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.MyMembership
import im.vector.matrix.android.internal.database.mapper.asEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
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.query.findAllIncludingEvents
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
import io.realm.Realm


@ -41,11 +44,16 @@ class RoomSyncHandler(private val monarchy: Monarchy) {

val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: RoomEntity(roomId)

if (roomEntity.membership == RoomEntity.Membership.INVITED) {
if (roomEntity.membership == MyMembership.INVITED) {
roomEntity.chunks.deleteAllFromRealm()
}

roomEntity.membership = RoomEntity.Membership.JOINED

roomEntity.membership = MyMembership.JOINED

if (roomSync.summary != null) {
handleRoomSummary(realm, roomId, roomSync.summary)
}
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
val chunkEntity = StateEventsChunkHandler().handle(realm, roomId, roomSync.state.events)
if (!roomEntity.chunks.contains(chunkEntity)) {
@ -67,7 +75,7 @@ class RoomSyncHandler(private val monarchy: Monarchy) {
InvitedRoomSync): RoomEntity {
val roomEntity = RoomEntity()
roomEntity.roomId = roomId
roomEntity.membership = RoomEntity.Membership.INVITED
roomEntity.membership = MyMembership.INVITED
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
val chunkEntity = handleListOfEvent(realm, roomId, roomSync.inviteState.events)
if (!roomEntity.chunks.contains(chunkEntity)) {
@ -82,10 +90,30 @@ class RoomSyncHandler(private val monarchy: Monarchy) {
roomSync: RoomSync): RoomEntity {
return RoomEntity().apply {
this.roomId = roomId
this.membership = RoomEntity.Membership.LEFT
this.membership = MyMembership.LEFT
}
}

private fun handleRoomSummary(realm: Realm,
roomId: String,
roomSummary: RoomSyncSummary) {

val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: RoomSummaryEntity(roomId)

if (roomSummary.heroes.isNotEmpty()) {
roomSummaryEntity.heroes.clear()
roomSummaryEntity.heroes.addAll(roomSummary.heroes)
}
if (roomSummary.invitedMembersCount != null) {
roomSummaryEntity.invitedMembersCount = roomSummary.invitedMembersCount
}
if (roomSummary.joinedMembersCount != null) {
roomSummaryEntity.joinedMembersCount = roomSummary.joinedMembersCount
}
realm.insertOrUpdate(roomSummaryEntity)
}

private fun handleListOfEvent(realm: Realm,
roomId: String,
eventList: List<Event>,

View File

@ -44,6 +44,12 @@ data class RoomSync(
/**
* The notification counts for the room.
*/
@Json(name = "unread_notifications") val unreadNotifications: RoomSyncUnreadNotifications? = null
@Json(name = "unread_notifications") val unreadNotifications: RoomSyncUnreadNotifications? = null,

/**
* The room summary
*/
@Json(name = "summary") val summary: RoomSyncSummary? = null


)

View File

@ -0,0 +1,32 @@
package im.vector.matrix.android.internal.session.sync.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass


@JsonClass(generateAdapter = true)
data class RoomSyncSummary(

/**
* Present only if the room has no m.room.name or m.room.canonical_alias.
*
*
* Lists the mxids of the first 5 members in the room who are currently joined or invited (ordered by stream ordering as seen on the server,
* to avoid it jumping around if/when topological order changes). As the heroes membership status changes, the list changes appropriately
* (sending the whole new list in the next /sync response). This list always excludes the current logged in user. If there are no joined or
* invited users, it lists the parted and banned ones instead. Servers can choose to send more or less than 5 members if they must, but 5
* seems like a good enough number for most naming purposes. Clients should use all the provided members to name the room, but may truncate
* the list if helpful for UX
*/
@Json(name = "m.heroes") val heroes: List<String> = emptyList(),

/**
* The number of m.room.members in state 'joined' (including the syncing user) (can be null)
*/
@Json(name = "m.joined_member_count") val joinedMembersCount: Int? = null,

/**
* The number of m.room.members in state 'invited' (can be null)
*/
@Json(name = "m.invited_member_count") val invitedMembersCount: Int? = null
)

View File

@ -84,4 +84,18 @@
<string name="reply_to_an_audio_file">sent an audio file.</string>
<string name="reply_to_a_file">sent a file.</string>

<!-- Room display name -->
<string name="room_displayname_invite_from">Invite from %s</string>
<string name="room_displayname_room_invite">Room Invite</string>

<!-- The 2 parameters will be members' name -->
<string name="room_displayname_two_members">%1$s and %2$s</string>

<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s and 1 other</item>
<item quantity="other">%1$s and %2$d others</item>
</plurals>

<string name="room_displayname_empty_room">Empty room</string>

</resources>