Merge branch 'feature/room_list' into develop

This commit is contained in:
ganfra 2019-01-31 12:15:43 +01:00
commit 30bfada5d2
52 changed files with 1095 additions and 207 deletions

View File

@ -11,6 +11,19 @@ androidExtensions {
experimental = true experimental = true
} }


def versionMajor = 0
def versionMinor = 1
def versionPatch = 0

def generateVersionCodeFromTimestamp() {
// It's unix timestamp divided by 10: It's incremented by one every 10 seconds.
return (System.currentTimeMillis() / 1_000 / 10).toInteger()
}

def generateVersionCodeFromVersionName() {
return versionMajor * 10000 + versionMinor * 100 + versionPatch
}

android { android {
compileSdkVersion 28 compileSdkVersion 28
defaultConfig { defaultConfig {
@ -18,8 +31,8 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 28 targetSdkVersion 28
multiDexEnabled true multiDexEnabled true
versionCode 1 versionCode generateVersionCodeFromTimestamp()
versionName "1.0" versionName "${versionMajor}.${versionMinor}.${versionPatch}"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {

View File

@ -16,10 +16,11 @@


package im.vector.riotredesign.core.epoxy package im.vector.riotredesign.core.epoxy


import android.view.View
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import android.view.View
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelVisibilityStateChangedListener
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty


@ -29,6 +30,7 @@ abstract class KotlinModel(


private var view: View? = null private var view: View? = null
private var onBindCallback: (() -> Unit)? = null private var onBindCallback: (() -> Unit)? = null
private var onModelVisibilityStateChangedListener: OnModelVisibilityStateChangedListener<KotlinModel, View>? = null


abstract fun bind() abstract fun bind()


@ -47,6 +49,16 @@ abstract class KotlinModel(
return this return this
} }


override fun onVisibilityStateChanged(visibilityState: Int, view: View) {
onModelVisibilityStateChangedListener?.onVisibilityStateChanged(this, view, visibilityState)
super.onVisibilityStateChanged(visibilityState, view)
}

fun setOnVisibilityStateChanged(listener: OnModelVisibilityStateChangedListener<KotlinModel, View>): KotlinModel {
this.onModelVisibilityStateChangedListener = listener
return this
}

override fun getDefaultLayout() = layoutRes override fun getDefaultLayout() = layoutRes


protected fun <V : View> bind(@IdRes id: Int) = object : ReadOnlyProperty<KotlinModel, V> { protected fun <V : View> bind(@IdRes id: Int) = object : ReadOnlyProperty<KotlinModel, V> {

View File

@ -0,0 +1,38 @@
/*
*
* * 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.core.extensions

/**
* Returns the last element yielding the smallest value of the given function or `null` if there are no elements.
*/
public inline fun <T, R : Comparable<R>> Iterable<T>.lastMinBy(selector: (T) -> R): T? {
val iterator = iterator()
if (!iterator.hasNext()) return null
var minElem = iterator.next()
var minValue = selector(minElem)
while (iterator.hasNext()) {
val e = iterator.next()
val v = selector(e)
if (minValue >= v) {
minElem = e
minValue = v
}
}
return minElem
}

View File

@ -27,6 +27,8 @@ import im.vector.riotredesign.features.home.room.detail.timeline.TimelineDateFor
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineItemFactory import im.vector.riotredesign.features.home.room.detail.timeline.TimelineItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
import im.vector.riotredesign.features.home.room.list.RoomSummaryController
import org.koin.dsl.module.module import org.koin.dsl.module.module


class HomeModule { class HomeModule {
@ -65,6 +67,10 @@ class HomeModule {
HomeNavigator() HomeNavigator()
} }


factory {
RoomSummaryController(get())
}

factory { (roomId: String) -> factory { (roomId: String) ->
TimelineEventController(roomId, get(), get(), get()) TimelineEventController(roomId, get(), get(), get())
} }
@ -85,6 +91,10 @@ class HomeModule {
HomePermalinkHandler(get()) HomePermalinkHandler(get())
} }


single {
RoomSummaryComparator()
}



} }
} }

View File

@ -16,9 +16,12 @@


package im.vector.riotredesign.features.home.room.detail package im.vector.riotredesign.features.home.room.detail


import im.vector.matrix.android.api.session.room.timeline.TimelineEvent

sealed class RoomDetailActions { sealed class RoomDetailActions {


data class SendMessage(val text: String) : RoomDetailActions() data class SendMessage(val text: String) : RoomDetailActions()
object IsDisplayed : RoomDetailActions() object IsDisplayed : RoomDetailActions()
data class EventDisplayed(val event: TimelineEvent, val index: Int) : RoomDetailActions()


} }

View File

@ -23,9 +23,11 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.ToolbarConfigurable
@ -75,7 +77,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {


override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
roomDetailViewModel.accept(RoomDetailActions.IsDisplayed) roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
} }


private fun setupToolbar() { private fun setupToolbar() {
@ -86,6 +88,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
} }


private fun setupRecyclerView() { private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView)
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
@ -100,7 +104,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
val textMessage = composerEditText.text.toString() val textMessage = composerEditText.text.toString()
if (textMessage.isNotBlank()) { if (textMessage.isNotBlank()) {
composerEditText.text = null composerEditText.text = null
roomDetailViewModel.accept(RoomDetailActions.SendMessage(textMessage)) roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
} }
} }
} }
@ -143,4 +147,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
homePermalinkHandler.launch(url) homePermalinkHandler.launch(url)
} }


override fun onEventVisible(event: TimelineEvent, index: Int) {
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event, index))
}

} }

View File

@ -18,14 +18,18 @@ package im.vector.riotredesign.features.home.room.detail


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 im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback 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.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.extensions.lastMinBy
import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.core.platform.RiotViewModel
import im.vector.riotredesign.features.home.room.VisibleRoomHolder import im.vector.riotredesign.features.home.room.VisibleRoomHolder
import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit


class RoomDetailViewModel(initialState: RoomDetailViewState, class RoomDetailViewModel(initialState: RoomDetailViewState,
private val session: Session, private val session: Session,
@ -36,6 +40,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
private val roomId = initialState.roomId private val roomId = initialState.roomId
private val eventId = initialState.eventId private val eventId = initialState.eventId


private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()

companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> { companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {


@JvmStatic @JvmStatic
@ -49,13 +55,15 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
init { init {
observeRoomSummary() observeRoomSummary()
observeTimeline() observeTimeline()
observeDisplayedEvents()
room.loadRoomMembersIfNeeded() room.loadRoomMembersIfNeeded()
} }


fun accept(action: RoomDetailActions) { fun process(action: RoomDetailActions) {
when (action) { when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.IsDisplayed -> visibleRoomHolder.setVisibleRoom(roomId) is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
} }
} }


@ -65,6 +73,29 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
room.sendTextMessage(action.text, callback = object : MatrixCallback<Event> {}) room.sendTextMessage(action.text, callback = object : MatrixCallback<Event> {})
} }


private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
displayedEventsObservable.accept(action)
}

private fun handleIsDisplayed() {
visibleRoomHolder.setVisibleRoom(roomId)
}

private fun observeDisplayedEvents() {
// We are buffering scroll events for one second
// and keep the most recent one to set the read receipt on.
displayedEventsObservable.hide()
.buffer(1, TimeUnit.SECONDS)
.filter { it.isNotEmpty() }
.subscribeBy(onNext = { actions ->
val mostRecentEvent = actions.lastMinBy { it.index }
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
room.setReadReceipt(eventId, callback = object : MatrixCallback<Void> {})
}
})
.disposeOnClear()
}

private fun observeRoomSummary() { private fun observeRoomSummary() {
room.rx().liveRoomSummary() room.rx().liveRoomSummary()
.execute { async -> .execute { async ->

View File

@ -16,12 +16,16 @@


package im.vector.riotredesign.features.home.room.detail.timeline package im.vector.riotredesign.features.home.room.detail.timeline


import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelVisibilityStateChangedListener
import com.airbnb.epoxy.VisibilityState
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineData import im.vector.matrix.android.api.session.room.timeline.TimelineData
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.KotlinModel
import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.LoadingItemModel_ import im.vector.riotredesign.features.home.LoadingItemModel_
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
@ -74,6 +78,11 @@ class TimelineEventController(private val roomId: String,


timelineItemFactory.create(event, nextEvent, callback)?.also { timelineItemFactory.create(event, nextEvent, callback)?.also {
it.id(event.localId) it.id(event.localId)
it.setOnVisibilityStateChanged(OnModelVisibilityStateChangedListener<KotlinModel, View> { model, view, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {
callback?.onEventVisible(event, currentPosition)
}
})
epoxyModels.add(it) epoxyModels.add(it)
} }
if (addDaySeparator) { if (addDaySeparator) {
@ -98,6 +107,7 @@ class TimelineEventController(private val roomId: String,




interface Callback { interface Callback {
fun onEventVisible(event: TimelineEvent, index: Int)
fun onUrlClicked(url: String) fun onUrlClicked(url: String)
} }



View File

@ -16,9 +16,9 @@


package im.vector.riotredesign.features.home.room.detail.timeline package im.vector.riotredesign.features.home.room.detail.timeline


import com.airbnb.epoxy.EpoxyModel
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.KotlinModel


class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
private val roomNameItemFactory: RoomNameItemFactory, private val roomNameItemFactory: RoomNameItemFactory,
@ -28,7 +28,7 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,


fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
callback: TimelineEventController.Callback?): EpoxyModel<*>? { callback: TimelineEventController.Callback?): KotlinModel? {


return when (event.root.type) { return when (event.root.type) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback) EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)

View File

@ -26,9 +26,12 @@ import im.vector.riotredesign.core.epoxy.KotlinModel
data class RoomCategoryItem( data class RoomCategoryItem(
val title: CharSequence, val title: CharSequence,
val isExpanded: Boolean, val isExpanded: Boolean,
val unreadCount: Int,
val showHighlighted: Boolean,
val listener: (() -> Unit)? = null val listener: (() -> Unit)? = null
) : KotlinModel(R.layout.item_room_category) { ) : KotlinModel(R.layout.item_room_category) {


private val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomCategoryUnreadCounterBadgeView)
private val titleView by bind<TextView>(R.id.roomCategoryTitleView) private val titleView by bind<TextView>(R.id.roomCategoryTitleView)
private val rootView by bind<ViewGroup>(R.id.roomCategoryRootView) private val rootView by bind<ViewGroup>(R.id.roomCategoryRootView)


@ -41,6 +44,7 @@ data class RoomCategoryItem(
val expandedArrowDrawable = ContextCompat.getDrawable(rootView.context, expandedArrowDrawableRes)?.also { val expandedArrowDrawable = ContextCompat.getDrawable(rootView.context, expandedArrowDrawableRes)?.also {
DrawableCompat.setTint(it, tintColor) DrawableCompat.setTint(it, tintColor)
} }
unreadCounterBadgeView.render(unreadCount, showHighlighted)
titleView.setCompoundDrawablesWithIntrinsicBounds(expandedArrowDrawable, null, null, null) titleView.setCompoundDrawablesWithIntrinsicBounds(expandedArrowDrawable, null, null, null)
titleView.text = title titleView.text = title
rootView.setOnClickListener { listener?.invoke() } rootView.setOnClickListener { listener?.invoke() }

View File

@ -22,7 +22,6 @@ import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
@ -30,7 +29,6 @@ import com.airbnb.mvrx.activityViewModel
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.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.extensions.hideKeyboard
import im.vector.riotredesign.core.extensions.setupAsSearch import im.vector.riotredesign.core.extensions.setupAsSearch
import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.StateView import im.vector.riotredesign.core.platform.StateView
@ -47,8 +45,8 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
} }


private val homeNavigator by inject<HomeNavigator>() private val homeNavigator by inject<HomeNavigator>()
private val roomController by inject<RoomSummaryController>()
private val homeViewModel: RoomListViewModel by activityViewModel() private val homeViewModel: RoomListViewModel by activityViewModel()
private lateinit var roomController: RoomSummaryController


override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_room_list, container, false) return inflater.inflate(R.layout.fragment_room_list, container, false)
@ -56,7 +54,7 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {


override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
roomController = RoomSummaryController(this) roomController.callback = this
stateView.contentView = epoxyRecyclerView stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(roomController) epoxyRecyclerView.setController(roomController)
setupFilterView() setupFilterView()

View File

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
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.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.core.platform.RiotViewModel
import im.vector.riotredesign.features.home.group.SelectedGroupHolder import im.vector.riotredesign.features.home.group.SelectedGroupHolder
@ -32,6 +33,7 @@ import io.reactivex.Observable
import io.reactivex.functions.Function3 import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit


typealias RoomListFilterName = CharSequence typealias RoomListFilterName = CharSequence


@ -39,7 +41,8 @@ class RoomListViewModel(initialState: RoomListViewState,
private val session: Session, private val session: Session,
private val selectedGroupHolder: SelectedGroupHolder, private val selectedGroupHolder: SelectedGroupHolder,
private val visibleRoomHolder: VisibleRoomHolder, private val visibleRoomHolder: VisibleRoomHolder,
private val roomSelectionRepository: RoomSelectionRepository) private val roomSelectionRepository: RoomSelectionRepository,
private val roomSummaryComparator: RoomSummaryComparator)
: RiotViewModel<RoomListViewState>(initialState) { : RiotViewModel<RoomListViewState>(initialState) {


companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> { companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> {
@ -50,7 +53,8 @@ class RoomListViewModel(initialState: RoomListViewState,
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>() val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupHolder>() val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupHolder>()
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomHolder>() val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomHolder>()
return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository) val roomSummaryComparator = viewModelContext.activity.get<RoomSummaryComparator>()
return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository, roomSummaryComparator)
} }
} }


@ -92,19 +96,11 @@ class RoomListViewModel(initialState: RoomListViewState,


private fun observeRoomSummaries() { private fun observeRoomSummaries() {
Observable.combineLatest<List<RoomSummary>, Option<GroupSummary>, Option<RoomListFilterName>, RoomSummaries>( Observable.combineLatest<List<RoomSummary>, Option<GroupSummary>, Option<RoomListFilterName>, RoomSummaries>(
session.rx().liveRoomSummaries(), session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
selectedGroupHolder.selectedGroup(), selectedGroupHolder.selectedGroup(),
roomListFilter, roomListFilter.throttleLast(300, TimeUnit.MILLISECONDS),
Function3 { rooms, selectedGroupOption, filterRoomOption -> Function3 { rooms, selectedGroupOption, filterRoomOption ->
val filterRoom = filterRoomOption.orNull() val filteredRooms = filterRooms(rooms, filterRoomOption)
val filteredRooms = rooms.filter {
if (filterRoom.isNullOrBlank()) {
true
} else {
it.displayName.contains(other = filterRoom, ignoreCase = true)
}
}

val selectedGroup = selectedGroupOption.orNull() val selectedGroup = selectedGroupOption.orNull()
val filteredDirectRooms = filteredRooms val filteredDirectRooms = filteredRooms
.filter { it.isDirect } .filter { it.isDirect }
@ -123,7 +119,7 @@ class RoomListViewModel(initialState: RoomListViewState,
.filter { .filter {
selectedGroup?.roomIds?.contains(it.roomId) ?: true selectedGroup?.roomIds?.contains(it.roomId) ?: true
} }
RoomSummaries(filteredDirectRooms, filteredGroupRooms) buildRoomSummaries(filteredDirectRooms + filteredGroupRooms)
} }
) )
.execute { async -> .execute { async ->
@ -132,4 +128,44 @@ class RoomListViewModel(initialState: RoomListViewState,
) )
} }
} }

private fun filterRooms(rooms: List<RoomSummary>, filterRoomOption: Option<RoomListFilterName>): List<RoomSummary> {
val filterRoom = filterRoomOption.orNull()
return rooms.filter {
if (filterRoom.isNullOrBlank()) {
true
} else {
it.displayName.contains(other = filterRoom, ignoreCase = true)
}
}
}

private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
val favourites = ArrayList<RoomSummary>()
val directChats = ArrayList<RoomSummary>()
val groupRooms = ArrayList<RoomSummary>()
val lowPriorities = ArrayList<RoomSummary>()
val serverNotices = ArrayList<RoomSummary>()

for (room in rooms) {
val tags = room.tags.map { it.name }
when {
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
room.isDirect -> directChats.add(room)
else -> groupRooms.add(room)
}
}

return RoomSummaries(
favourites = favourites.sortedWith(roomSummaryComparator),
directRooms = directChats.sortedWith(roomSummaryComparator),
groupRooms = groupRooms.sortedWith(roomSummaryComparator),
lowPriorities = lowPriorities.sortedWith(roomSummaryComparator),
serverNotices = serverNotices.sortedWith(roomSummaryComparator)
)
}


} }

View File

@ -27,10 +27,13 @@ data class RoomListViewState(
) : MvRxState ) : MvRxState


data class RoomSummaries( data class RoomSummaries(
val favourites: List<RoomSummary>,
val directRooms: List<RoomSummary>, val directRooms: List<RoomSummary>,
val groupRooms: List<RoomSummary> val groupRooms: List<RoomSummary>,
val lowPriorities: List<RoomSummary>,
val serverNotices: List<RoomSummary>
) )


fun RoomSummaries?.isNullOrEmpty(): Boolean { fun RoomSummaries?.isNullOrEmpty(): Boolean {
return this == null || (directRooms.isEmpty() && groupRooms.isEmpty()) return this == null || (directRooms.isEmpty() && groupRooms.isEmpty() && favourites.isEmpty() && lowPriorities.isEmpty() && serverNotices.isEmpty())
} }

View File

@ -0,0 +1,70 @@
/*
* 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.room.model.RoomSummary

class RoomSummaryComparator
: Comparator<RoomSummary> {

override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
val retValue: Int
var leftHighlightCount = 0
var rightHighlightCount = 0
var leftNotificationCount = 0
var rightNotificationCount = 0
var rightTimestamp = 0L
var leftTimestamp = 0L

if (null != leftRoomSummary) {
leftHighlightCount = leftRoomSummary.highlightCount
leftNotificationCount = leftRoomSummary.notificationCount
leftTimestamp = leftRoomSummary.lastMessage?.originServerTs ?: 0
}
if (null != rightRoomSummary) {
rightHighlightCount = rightRoomSummary.highlightCount
rightNotificationCount = rightRoomSummary.notificationCount
rightTimestamp = rightRoomSummary.lastMessage?.originServerTs ?: 0
}

if (leftRoomSummary?.lastMessage == null) {
retValue = 1
} else if (rightRoomSummary?.lastMessage == null) {
retValue = -1
} else if (rightHighlightCount > 0 && leftHighlightCount == 0) {
retValue = 1
} else if (rightHighlightCount == 0 && leftHighlightCount > 0) {
retValue = -1
} else if (rightNotificationCount > 0 && leftNotificationCount == 0) {
retValue = 1
} else if (rightNotificationCount == 0 && leftNotificationCount > 0) {
retValue = -1
} else {
val deltaTimestamp = rightTimestamp - leftTimestamp
if (deltaTimestamp > 0) {
retValue = 1
} else if (deltaTimestamp < 0) {
retValue = -1
} else {
retValue = 0
}
}
return retValue

}

}

View File

@ -16,56 +16,100 @@


package im.vector.riotredesign.features.home.room.list package im.vector.riotredesign.features.home.room.list


import androidx.annotation.StringRes
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
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.core.resources.StringProvider


class RoomSummaryController(private val callback: Callback? = null class RoomSummaryController(private val stringProvider: StringProvider
) : TypedEpoxyController<RoomListViewState>() { ) : TypedEpoxyController<RoomListViewState>() {


private var isDirectRoomsExpanded = true private var isFavoriteRoomsExpanded = true
private var isGroupRoomsExpanded = true private var isDirectRoomsExpanded = false
private var isGroupRoomsExpanded = false
private var isLowPriorityRoomsExpanded = false
private var isServerNoticeRoomsExpanded = false

var callback: Callback? = null


override fun buildModels(viewState: RoomListViewState) { override fun buildModels(viewState: RoomListViewState) {
val roomSummaries = viewState.asyncRooms() val roomSummaries = viewState.asyncRooms()
RoomCategoryItem( val favourites = roomSummaries?.favourites ?: emptyList()
title = "DIRECT MESSAGES", buildRoomCategory(viewState, favourites, R.string.room_list_favourites, isFavoriteRoomsExpanded) {
isExpanded = isDirectRoomsExpanded, isFavoriteRoomsExpanded = !isFavoriteRoomsExpanded
listener = { }
if (isFavoriteRoomsExpanded) {
buildRoomModels(favourites, viewState.selectedRoomId)
}

val directRooms = roomSummaries?.directRooms ?: emptyList()
buildRoomCategory(viewState, directRooms, R.string.room_list_direct, isDirectRoomsExpanded) {
isDirectRoomsExpanded = !isDirectRoomsExpanded isDirectRoomsExpanded = !isDirectRoomsExpanded
setData(viewState)
} }
)
.id("direct_messages")
.addTo(this)

if (isDirectRoomsExpanded) { if (isDirectRoomsExpanded) {
buildRoomModels(roomSummaries?.directRooms ?: emptyList(), viewState.selectedRoomId) buildRoomModels(directRooms, viewState.selectedRoomId)
} }


RoomCategoryItem( val groupRooms = roomSummaries?.groupRooms ?: emptyList()
title = "GROUPS", buildRoomCategory(viewState, groupRooms, R.string.room_list_group, isGroupRoomsExpanded) {
isExpanded = isGroupRoomsExpanded,
listener = {
isGroupRoomsExpanded = !isGroupRoomsExpanded isGroupRoomsExpanded = !isGroupRoomsExpanded
}
if (isGroupRoomsExpanded) {
buildRoomModels(groupRooms, viewState.selectedRoomId)
}

val lowPriorities = roomSummaries?.lowPriorities ?: emptyList()
buildRoomCategory(viewState, lowPriorities, R.string.room_list_low_priority, isLowPriorityRoomsExpanded) {
isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded
}
if (isLowPriorityRoomsExpanded) {
buildRoomModels(lowPriorities, viewState.selectedRoomId)
}

val serverNotices = roomSummaries?.serverNotices ?: emptyList()
buildRoomCategory(viewState, serverNotices, R.string.room_list_system_alert, isServerNoticeRoomsExpanded) {
isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded
}
if (isServerNoticeRoomsExpanded) {
buildRoomModels(serverNotices, viewState.selectedRoomId)
}

}

private fun buildRoomCategory(viewState: RoomListViewState, summaries: List<RoomSummary>, @StringRes titleRes: Int, isExpanded: Boolean, mutateExpandedState: () -> Unit) {
//TODO should add some business logic later
val unreadCount = if (summaries.isEmpty()) {
0
} else {
summaries.map { it.notificationCount }.reduce { acc, i -> acc + i }
}
val showHighlighted = summaries.any { it.highlightCount > 0 }
RoomCategoryItem(
title = stringProvider.getString(titleRes).toUpperCase(),
isExpanded = isExpanded,
unreadCount = unreadCount,
showHighlighted = showHighlighted,
listener = {
mutateExpandedState()
setData(viewState) setData(viewState)
} }
) )
.id("group_messages") .id(titleRes)
.addTo(this) .addTo(this)

if (isGroupRoomsExpanded) {
buildRoomModels(roomSummaries?.groupRooms ?: emptyList(), viewState.selectedRoomId)
}

} }


private fun buildRoomModels(summaries: List<RoomSummary>, selectedRoomId: String?) { private fun buildRoomModels(summaries: List<RoomSummary>, selectedRoomId: String?) {
summaries.forEach { roomSummary -> summaries.forEach { roomSummary ->
val unreadCount = roomSummary.notificationCount
val showHighlighted = roomSummary.highlightCount > 0
val isSelected = roomSummary.roomId == selectedRoomId val isSelected = roomSummary.roomId == selectedRoomId
RoomSummaryItem( RoomSummaryItem(
roomName = roomSummary.displayName, roomName = roomSummary.displayName,
avatarUrl = roomSummary.avatarUrl, avatarUrl = roomSummary.avatarUrl,
isSelected = isSelected, isSelected = isSelected,
showHighlighted = showHighlighted,
unreadCount = unreadCount,
listener = { callback?.onRoomSelected(roomSummary) } listener = { callback?.onRoomSelected(roomSummary) }
) )
.id(roomSummary.roomId) .id(roomSummary.roomId)

View File

@ -14,24 +14,22 @@
* limitations under the License. * limitations under the License.
*/ */


package im.vector.matrix.android.internal.database.mapper package im.vector.riotredesign.features.home.room.list


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



/**
internal object RoomMapper { * Format the unread messages counter.

*

* @param count the count
fun map(roomEntity: RoomEntity): Room { * @return the formatted value
return DefaultRoom( */
roomEntity.roomId, fun formatUnreadMessagesCounter(count: Int): String {
roomEntity.membership return if (count > 999) {
) "${count / 1000}.${count % 1000 / 100}K"
} else {
count.toString()
} }
} }


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

View File

@ -28,14 +28,18 @@ data class RoomSummaryItem(
val roomName: CharSequence, val roomName: CharSequence,
val avatarUrl: String?, val avatarUrl: String?,
val isSelected: Boolean, val isSelected: Boolean,
val unreadCount: Int,
val showHighlighted: Boolean,
val listener: (() -> Unit)? = null val listener: (() -> Unit)? = null
) : KotlinModel(R.layout.item_room) { ) : KotlinModel(R.layout.item_room) {


private val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
private val titleView by bind<TextView>(R.id.roomNameView) private val titleView by bind<TextView>(R.id.roomNameView)
private val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView) private val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
private val rootView by bind<CheckableFrameLayout>(R.id.itemRoomLayout) private val rootView by bind<CheckableFrameLayout>(R.id.itemRoomLayout)


override fun bind() { override fun bind() {
unreadCounterBadgeView.render(unreadCount, showHighlighted)
rootView.isChecked = isSelected rootView.isChecked = isSelected
rootView.setOnClickListener { listener?.invoke() } rootView.setOnClickListener { listener?.invoke() }
titleView.text = roomName titleView.text = roomName

View File

@ -0,0 +1,52 @@
/*
* 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.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.AppCompatTextView
import im.vector.riotredesign.R

class UnreadCounterBadgeView : AppCompatTextView {

constructor(context: Context) : super(context)

constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

fun render(count: Int, highlighted: Boolean) {
if (count == 0) {
visibility = View.INVISIBLE
} else {
visibility = View.VISIBLE
val bgRes = if (highlighted) {
R.drawable.bg_unread_highlight
} else {
R.drawable.bg_unread_notification
}
setBackgroundResource(bgRes)
text = RoomSummaryFormatter.formatUnreadMessagesCounter(count)
}
}

enum class Status {
NOTIFICATION,
HIGHLIGHT
}

}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/rosy_pink" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>

<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/grey_lynch" />
</shape>

View File

@ -1,18 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>


<im.vector.riotredesign.core.platform.CheckableFrameLayout <im.vector.riotredesign.core.platform.CheckableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemRoomLayout" android:id="@+id/itemRoomLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:paddingRight="16dp"
android:foreground="?attr/selectableItemBackground"
android:background="@drawable/bg_room_item" android:background="@drawable/bg_room_item"
android:clickable="true" android:clickable="true"
android:focusable="true"> android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:paddingLeft="8dp"
android:paddingRight="16dp">


<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -35,15 +34,34 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:duplicateParentState="true" android:duplicateParentState="true"
android:textColor="@color/color_room_title" android:textColor="@color/color_room_title"
android:textSize="14sp" android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@+id/roomUnreadCounterBadgeView"
app:layout_constraintStart_toEndOf="@id/roomAvatarImageView" app:layout_constraintStart_toEndOf="@id/roomAvatarImageView"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" /> tools:text="@tools:sample/full_names" />


<im.vector.riotredesign.features.home.room.list.UnreadCounterBadgeView
android:id="@+id/roomUnreadCounterBadgeView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:minWidth="24dp"
android:minHeight="24dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:textColor="@android:color/white"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:background="@drawable/bg_unread_highlight"
tools:text="115" />

</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>


</im.vector.riotredesign.core.platform.CheckableFrameLayout> </im.vector.riotredesign.core.platform.CheckableFrameLayout>

View File

@ -11,7 +11,7 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingLeft="16dp" android:paddingLeft="16dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingRight="16dp" android:paddingRight="8dp"
android:paddingBottom="8dp" android:paddingBottom="8dp"
tools:background="@color/pale_grey"> tools:background="@color/pale_grey">


@ -26,16 +26,32 @@
android:text="DIRECT MESSAGES" android:text="DIRECT MESSAGES"
android:textColor="@color/bluey_grey_two" android:textColor="@color/bluey_grey_two"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/roomCategoryAddButton" app:layout_constraintEnd_toStartOf="@+id/roomCategoryUnreadCounterBadgeView"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />


<im.vector.riotredesign.features.home.room.list.UnreadCounterBadgeView
android:id="@+id/roomCategoryUnreadCounterBadgeView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:minWidth="24dp"
android:minHeight="24dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:textColor="@android:color/white"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/roomCategoryAddButton"
app:layout_constraintTop_toTopOf="parent"
tools:background="@drawable/bg_unread_highlight"
tools:text="4" />

<ImageView <ImageView
android:id="@+id/roomCategoryAddButton" android:id="@+id/roomCategoryAddButton"
android:layout_width="32dp" android:layout_width="40dp"
android:layout_height="32dp" android:layout_height="40dp"

android:scaleType="centerInside" android:scaleType="centerInside"
android:src="@drawable/ic_add_circle_white" android:src="@drawable/ic_add_circle_white"
android:tint="@color/bluey_grey_two" android:tint="@color/bluey_grey_two"

View File

@ -16,4 +16,5 @@
<color name="cool_grey">#a5aab2</color> <color name="cool_grey">#a5aab2</color>
<color name="pale_grey_two">#ebedf8</color> <color name="pale_grey_two">#ebedf8</color>
<color name="brown_grey">#a5a5a5</color> <color name="brown_grey">#a5a5a5</color>
<color name="grey_lynch">#61708B</color>
</resources> </resources>

View File

@ -1,9 +1,15 @@
<resources> <resources>
<string name="app_name">Riot X</string> <string name="app_name">"Riot X"</string>


<string name="global_retry">Réessayer</string> <string name="global_retry">"Retry"</string>
<string name="error_no_network">Pas de connexion internet</string> <string name="error_no_network">"No network connection"</string>
<string name="error_common">Une erreur est survenue</string> <string name="error_common">"An error occurred"</string>
<string name="room_list_empty">Rejoignez une room pour commencer à utiliser l\'application</string>
<string name="room_list_empty">"Join a room to start using the app."</string>
<string name="room_list_favourites">"Favourites"</string>
<string name="room_list_direct">"People"</string>
<string name="room_list_group">"Rooms"</string>
<string name="room_list_low_priority">"Low priority"</string>
<string name="room_list_system_alert">"System Alerts"</string>


</resources> </resources>

View File

@ -17,8 +17,8 @@
package im.vector.matrix.android.api.session.room package im.vector.matrix.android.api.session.room


import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.room.model.MyMembership
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.read.ReadService
import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
@ -26,20 +26,16 @@ import im.vector.matrix.android.api.util.Cancelable
/** /**
* This interface defines methods to interact within a room. * This interface defines methods to interact within a room.
*/ */
interface Room : TimelineService, SendService { interface Room : TimelineService, SendService, ReadService {


/** /**
* The roomId of this room * The roomId of this room
*/ */
val roomId: String val roomId: String


/**
* The membership of this room for the current user
*/
val myMembership: MyMembership

/** /**
* A live [RoomSummary] associated with the room * A live [RoomSummary] associated with the room
* You can observe this summary to get dynamic data from this room.
*/ */
val roomSummary: LiveData<RoomSummary> val roomSummary: LiveData<RoomSummary>



View File

@ -27,7 +27,7 @@ interface RoomService {
/** /**
* Get a room from a roomId * Get a room from a roomId
* @param roomId the roomId to look for. * @param roomId the roomId to look for.
* @return the room with roomId or null * @return a room with roomId or null
*/ */
fun getRoom(roomId: String): Room? fun getRoom(roomId: String): Room?



View File

@ -16,6 +16,9 @@


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

/** /**
* This class holds some data of a room. * This class holds some data of a room.
* It can be retrieved by [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService] * It can be retrieved by [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService]
@ -26,5 +29,9 @@ data class RoomSummary(
val topic: String = "", val topic: String = "",
val avatarUrl: String = "", val avatarUrl: String = "",
val isDirect: Boolean, val isDirect: Boolean,
val otherMemberIds: List<String> = emptyList() val lastMessage: Event? = null,
val otherMemberIds: List<String> = emptyList(),
var notificationCount: Int = 0,
var highlightCount: Int = 0,
var tags: List<RoomTag> = emptyList()
) )

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.api.session.room.model.tag

data class RoomTag(
val name: String,
val order: Double?
) {

companion object {
val ROOM_TAG_FAVOURITE = "m.favourite"
val ROOM_TAG_LOW_PRIORITY = "m.lowpriority"
val ROOM_TAG_NO_TAG = "m.recent"
val ROOM_TAG_SERVER_NOTICE = "m.server_notice"
}

}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.api.session.room.model.tag

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

@JsonClass(generateAdapter = true)
data class RoomTagContent(
@Json(name = "tags") val tags: Map<String, Map<String, Double>> = emptyMap()
)

View File

@ -0,0 +1,41 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.api.session.room.read

import im.vector.matrix.android.api.MatrixCallback

/**
* This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level.
*/
interface ReadService {

/**
* Force the read marker to be set on the latest event.
*/
fun markAllAsRead(callback: MatrixCallback<Void>)

/**
* Set the read receipt on the event with provided eventId.
*/
fun setReadReceipt(eventId: String, callback: MatrixCallback<Void>)

/**
* Set the read marker on the event with provided eventId.
*/
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Void>)

}

View File

@ -17,19 +17,27 @@
package im.vector.matrix.android.internal.database.mapper package im.vector.matrix.android.internal.database.mapper


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.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity




internal object RoomSummaryMapper { internal object RoomSummaryMapper {


fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
val tags = roomSummaryEntity.tags.map {
RoomTag(it.tagName, it.tagOrder)
}
return RoomSummary( return RoomSummary(
roomSummaryEntity.roomId, roomId = roomSummaryEntity.roomId,
roomSummaryEntity.displayName ?: "", displayName = roomSummaryEntity.displayName ?: "",
roomSummaryEntity.topic ?: "", topic = roomSummaryEntity.topic ?: "",
roomSummaryEntity.avatarUrl ?: "", avatarUrl = roomSummaryEntity.avatarUrl ?: "",
roomSummaryEntity.isDirect, isDirect = roomSummaryEntity.isDirect,
roomSummaryEntity.otherMemberIds.toList() lastMessage = roomSummaryEntity.lastMessage?.asDomain(),
otherMemberIds = roomSummaryEntity.otherMemberIds.toList(),
highlightCount = roomSummaryEntity.highlightCount,
notificationCount = roomSummaryEntity.notificationCount,
tags = tags
) )
} }
} }

View File

@ -29,7 +29,10 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var joinedMembersCount: Int? = 0, var joinedMembersCount: Int? = 0,
var invitedMembersCount: Int? = 0, var invitedMembersCount: Int? = 0,
var isDirect: Boolean = false, var isDirect: Boolean = false,
var otherMemberIds: RealmList<String> = RealmList() var otherMemberIds: RealmList<String> = RealmList(),
var notificationCount: Int = 0,
var highlightCount: Int = 0,
var tags: RealmList<RoomTagEntity> = RealmList()
) : RealmObject() { ) : RealmObject() {


companion object companion object

View File

@ -0,0 +1,28 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.database.model

import io.realm.RealmObject

internal open class RoomTagEntity(
var tagName: String = "",
var tagOrder: Double? = null
) : RealmObject() {

companion object

}

View File

@ -16,6 +16,7 @@


package im.vector.matrix.android.internal.database.query package im.vector.matrix.android.internal.database.query


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
@ -48,6 +49,21 @@ internal fun EventEntity.Companion.where(realm: Realm,
} }
} }


internal fun EventEntity.Companion.latestEvent(realm: Realm,
roomId: String,
includedTypes: List<String> = emptyList(),
excludedTypes: List<String> = emptyList()): EventEntity? {
val query = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.events?.where()
if (includedTypes.isNotEmpty()) {
query?.`in`(EventEntityFields.TYPE, includedTypes.toTypedArray())
} else if (excludedTypes.isNotEmpty()) {
query?.not()?.`in`(EventEntityFields.TYPE, excludedTypes.toTypedArray())
}
return query
?.sort(EventEntityFields.DISPLAY_INDEX)
?.findFirst()
}



internal fun RealmQuery<EventEntity>.next(from: Int? = null, strict: Boolean = true): EventEntity? { internal fun RealmQuery<EventEntity>.next(from: Int? = null, strict: Boolean = true): EventEntity? {
if (from != null) { if (from != null) {
@ -62,7 +78,6 @@ internal fun RealmQuery<EventEntity>.next(from: Int? = null, strict: Boolean = t
.findFirst() .findFirst()
} }



internal fun RealmQuery<EventEntity>.last(since: Int? = null, strict: Boolean = false): EventEntity? { internal fun RealmQuery<EventEntity>.last(since: Int? = null, strict: Boolean = false): EventEntity? {
if (since != null) { if (since != null) {
if (strict) { if (strict) {

View File

@ -18,11 +18,11 @@ package im.vector.matrix.android.internal.di


import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.network.UnitConverterFactory
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import okreplay.OkReplayInterceptor import okreplay.OkReplayInterceptor
import org.koin.dsl.module.module import org.koin.dsl.module.module
import retrofit2.Converter
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import timber.log.Timber import timber.log.Timber
@ -62,10 +62,6 @@ class NetworkModule {
MoshiProvider.providesMoshi() MoshiProvider.providesMoshi()
} }


single {
MoshiConverterFactory.create(get()) as Converter.Factory
}

single { single {
NetworkConnectivityChecker(get()) NetworkConnectivityChecker(get())
} }
@ -73,7 +69,8 @@ class NetworkModule {
factory { factory {
Retrofit.Builder() Retrofit.Builder()
.client(get()) .client(get())
.addConverterFactory(get()) .addConverterFactory(UnitConverterFactory)
.addConverterFactory(MoshiConverterFactory.create(get()))
} }


} }

View File

@ -0,0 +1,35 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.network

import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type

object UnitConverterFactory : Converter.Factory() {
override fun responseBodyConverter(type: Type, annotations: Array<out Annotation>,
retrofit: Retrofit): Converter<ResponseBody, *>? {
return if (type == Unit::class.java) UnitConverter else null
}

private object UnitConverter : Converter<ResponseBody, Unit> {
override fun convert(value: ResponseBody) {
value.close()
}
}
}

View File

@ -28,6 +28,7 @@ import im.vector.matrix.android.internal.session.group.DefaultGroupService
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.room.DefaultRoomService import im.vector.matrix.android.internal.session.room.DefaultRoomService
import im.vector.matrix.android.internal.session.room.RoomAvatarResolver import im.vector.matrix.android.internal.session.room.RoomAvatarResolver
import im.vector.matrix.android.internal.session.room.RoomFactory
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayNameResolver import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayNameResolver
@ -46,6 +47,10 @@ internal class SessionModule(private val sessionParams: SessionParams) {
sessionParams sessionParams
} }


scope(DefaultSession.SCOPE) {
sessionParams.credentials
}

scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
val context = get<Context>() val context = get<Context>()
val childPath = sessionParams.credentials.userId.md5() val childPath = sessionParams.credentials.userId.md5()
@ -84,10 +89,9 @@ internal class SessionModule(private val sessionParams: SessionParams) {
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
DefaultRoomService(get()) as RoomService DefaultRoomService(get(), get()) as RoomService
} }



scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
DefaultGroupService(get()) as GroupService DefaultGroupService(get()) as GroupService
} }

View File

@ -19,37 +19,35 @@ package im.vector.matrix.android.internal.session.room
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.send.SendService
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.MyMembership
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.timeline.TimelineData import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.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.di.MatrixKoinComponent
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
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
import org.koin.core.parameter.parametersOf
import org.koin.standalone.inject


internal data class DefaultRoom( internal data class DefaultRoom(
override val roomId: String, override val roomId: String,
override val myMembership: MyMembership private val loadRoomMembersTask: LoadRoomMembersTask,
) : Room, MatrixKoinComponent { private val monarchy: Monarchy,
private val timelineService: TimelineService,
private val sendService: SendService,
private val readService: ReadService,
private val taskExecutor: TaskExecutor


private val loadRoomMembersTask by inject<LoadRoomMembersTask>()
private val monarchy by inject<Monarchy>() ) : Room,
private val timelineService by inject<TimelineService> { parametersOf(roomId) } TimelineService by timelineService,
private val sendService by inject<SendService> { parametersOf(roomId) } SendService by sendService,
private val taskExecutor by inject<TaskExecutor>() ReadService by readService {


override val roomSummary: LiveData<RoomSummary> by lazy { override val roomSummary: LiveData<RoomSummary> by lazy {
val liveData = monarchy val liveData = monarchy
@ -62,19 +60,8 @@ internal data class DefaultRoom(
} }
} }


override fun timeline(eventId: String?): LiveData<TimelineData> {
return timelineService.timeline(eventId)
}

override fun loadRoomMembersIfNeeded(): Cancelable { override fun loadRoomMembersIfNeeded(): Cancelable {
val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE) val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE)
return loadRoomMembersTask.configureWith(params).executeBy(taskExecutor) return loadRoomMembersTask.configureWith(params).executeBy(taskExecutor)
} }


override fun sendTextMessage(text: String, callback: MatrixCallback<Event>): Cancelable {
return sendService.sendTextMessage(text, callback)
}


} }

View File

@ -26,15 +26,14 @@ 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.util.fetchCopied


internal class DefaultRoomService(private val monarchy: Monarchy) : RoomService { internal class DefaultRoomService(private val monarchy: Monarchy,
private val roomFactory: RoomFactory) : RoomService {


override fun getRoom(roomId: String): Room? { override fun getRoom(roomId: String): Room? {
var room: Room? = null monarchy.fetchCopied { RoomEntity.where(it, roomId).findFirst() } ?: return null
monarchy.doWithRealm { realm -> return roomFactory.instantiate(roomId)
room = RoomEntity.where(realm, roomId).findFirst()?.asDomain()
}
return room
} }


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

View File

@ -24,7 +24,12 @@ import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
import retrofit2.Call import retrofit2.Call
import retrofit2.http.* import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query


internal interface RoomAPI { internal interface RoomAPI {


@ -100,5 +105,14 @@ internal interface RoomAPI {
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/event/{eventId}") @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/event/{eventId}")
fun getEvent(@Path("roomId") roomId: String, @Path("eventId") eventId: String): Call<Event> fun getEvent(@Path("roomId") roomId: String, @Path("eventId") eventId: String): Call<Event>


/**
* Send read markers.
*
* @param roomId the room id
* @param markers the read markers
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers")
fun sendReadMarker(@Path("roomId") roomId: String, @Body markers: Map<String, String>): Call<Unit>



} }

View File

@ -20,11 +20,11 @@ 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.events.model.EventType 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.events.model.toModel
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.MyMembership
import im.vector.matrix.android.api.session.room.model.RoomAvatarContent import im.vector.matrix.android.api.session.room.model.RoomAvatarContent
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity 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.query.last import im.vector.matrix.android.internal.database.query.last
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.room.members.RoomMembers import im.vector.matrix.android.internal.session.room.members.RoomMembers
@ -34,20 +34,21 @@ internal class RoomAvatarResolver(private val monarchy: Monarchy,


/** /**
* Compute the room avatar url * Compute the room avatar url
* * @param roomId the roomId of the room to resolve avatar
* @return the room avatar url, can be a fallback to a room member avatar or null * @return the room avatar url, can be a fallback to a room member avatar or null
*/ */
fun resolve(room: Room): String? { fun resolve(roomId: String): String? {
var res: String? = null var res: String? = null
monarchy.doWithRealm { realm -> monarchy.doWithRealm { realm ->
val roomName = EventEntity.where(realm, room.roomId, EventType.STATE_ROOM_AVATAR).last()?.asDomain() val roomEntity = RoomEntity.where(realm, roomId).findFirst()
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).last()?.asDomain()
res = roomName?.content.toModel<RoomAvatarContent>()?.avatarUrl res = roomName?.content.toModel<RoomAvatarContent>()?.avatarUrl
if (!res.isNullOrEmpty()) { if (!res.isNullOrEmpty()) {
return@doWithRealm return@doWithRealm
} }
val roomMembers = RoomMembers(realm, room.roomId) val roomMembers = RoomMembers(realm, roomId)
val members = roomMembers.getLoaded() val members = roomMembers.getLoaded()
if (room.myMembership == MyMembership.INVITED) { if (roomEntity?.membership == MyMembership.INVITED) {
if (members.size == 1) { if (members.size == 1) {
res = members.entries.first().value.avatarUrl res = members.entries.first().value.avatarUrl
} else if (members.size > 1) { } else if (members.size > 1) {

View File

@ -0,0 +1,63 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.room

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
import im.vector.matrix.android.internal.session.room.read.DefaultReadService
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
import im.vector.matrix.android.internal.session.room.send.EventFactory
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.PaginationTask
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.PagingRequestHelper
import java.util.concurrent.Executors

internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
private val monarchy: Monarchy,
private val credentials: Credentials,
private val paginationTask: PaginationTask,
private val contextOfEventTask: GetContextOfEventTask,
private val setReadMarkersTask: SetReadMarkersTask,
private val eventFactory: EventFactory,
private val taskExecutor: TaskExecutor) {

fun instantiate(roomId: String): Room {
val helper = PagingRequestHelper(Executors.newSingleThreadExecutor())
val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, helper)
val roomMemberExtractor = RoomMemberExtractor(monarchy, roomId)
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineBoundaryCallback, contextOfEventTask, roomMemberExtractor)
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor)
return DefaultRoom(
roomId,
loadRoomMembersTask,
monarchy,
timelineService,
sendService,
readService,
taskExecutor
)
}

}

View File

@ -16,26 +16,19 @@


package im.vector.matrix.android.internal.session.room package im.vector.matrix.android.internal.session.room


import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.internal.session.room.send.EventFactory
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.internal.session.DefaultSession import im.vector.matrix.android.internal.session.DefaultSession
import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
import im.vector.matrix.android.internal.session.room.send.DefaultSendService import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.session.room.send.EventFactory
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
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.PaginationTask import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import im.vector.matrix.android.internal.util.PagingRequestHelper
import org.koin.dsl.module.module import org.koin.dsl.module.module
import retrofit2.Retrofit import retrofit2.Retrofit
import java.util.concurrent.Executors




class RoomModule { class RoomModule {
@ -64,19 +57,15 @@ class RoomModule {
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
val sessionParams = get<SessionParams>() DefaultSetReadMarkersTask(get(), get(),get()) as SetReadMarkersTask
EventFactory(sessionParams.credentials)
} }


factory { (roomId: String) -> scope(DefaultSession.SCOPE) {
val helper = PagingRequestHelper(Executors.newSingleThreadExecutor()) EventFactory(get())
val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, get(), get(), get(), helper)
val roomMemberExtractor = RoomMemberExtractor(get(), roomId)
DefaultTimelineService(roomId, get(), get(), timelineBoundaryCallback, get(), roomMemberExtractor) as TimelineService
} }


factory { (roomId: String) -> scope(DefaultSession.SCOPE) {
DefaultSendService(roomId, get(), get()) as SendService RoomFactory(get(), get(), get(), get(), get(), get(), get(), get())
} }


} }

View File

@ -21,7 +21,6 @@ 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.events.model.EventType 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.events.model.toModel
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
@ -29,6 +28,7 @@ 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.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.query.last import im.vector.matrix.android.internal.database.query.last
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.members.RoomMembers import im.vector.matrix.android.internal.session.room.members.RoomMembers
@ -45,28 +45,28 @@ internal class RoomSummaryUpdater(monarchy: Monarchy,
override val query = Monarchy.Query<RoomEntity> { RoomEntity.where(it) } override val query = Monarchy.Query<RoomEntity> { RoomEntity.where(it) }


override fun process(inserted: List<RoomEntity>, updated: List<RoomEntity>, deleted: List<RoomEntity>) { override fun process(inserted: List<RoomEntity>, updated: List<RoomEntity>, deleted: List<RoomEntity>) {
val rooms = (inserted + updated).map { it.asDomain() } val rooms = (inserted + updated).map { it.roomId }
monarchy.writeAsync { realm -> monarchy.writeAsync { realm ->
rooms.forEach { updateRoom(realm, it) } rooms.forEach { updateRoom(realm, it) }
} }
} }


private fun updateRoom(realm: Realm, room: Room?) { private fun updateRoom(realm: Realm, roomId: String?) {
if (room == null) { if (roomId == null) {
return return
} }
val roomSummary = RoomSummaryEntity.where(realm, room.roomId).findFirst() val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(room.roomId) ?: realm.createObject(roomId)


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


val otherRoomMembers = RoomMembers(realm, room.roomId).getLoaded().filterKeys { it != credentials.userId } val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId }


roomSummary.displayName = roomDisplayNameResolver.resolve(context, room).toString() roomSummary.displayName = roomDisplayNameResolver.resolve(context, roomId).toString()
roomSummary.avatarUrl = roomAvatarResolver.resolve(room) roomSummary.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummary.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic roomSummary.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
roomSummary.lastMessage = lastMessageEvent roomSummary.lastMessage = lastEvent
roomSummary.otherMemberIds.clear() roomSummary.otherMemberIds.clear()
roomSummary.otherMemberIds.addAll(otherRoomMembers.keys) roomSummary.otherMemberIds.addAll(otherRoomMembers.keys)
} }

View File

@ -22,13 +22,13 @@ import im.vector.matrix.android.R
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.events.model.EventType 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.events.model.toModel
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.MyMembership
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent 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.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomNameContent import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity 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.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.last import im.vector.matrix.android.internal.database.query.last
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
@ -45,10 +45,10 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
* Compute the room display name * Compute the room display name
* *
* @param context * @param context
* @param room: the room to resolve the name of. * @param roomId: the roomId to resolve the name of.
* @return the room display name * @return the room display name
*/ */
fun resolve(context: Context, room: Room): CharSequence { fun resolve(context: Context, roomId: String): CharSequence {
// this algorithm is the one defined in // this algorithm is the one defined in
// https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617 // https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617
// calculateRoomName(room, userId) // calculateRoomName(room, userId)
@ -57,29 +57,30 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
// https://docs.google.com/document/d/11i14UI1cUz-OJ0knD5BFu7fmT6Fo327zvMYqfSAR7xs/edit#heading=h.qif6pkqyjgzn // https://docs.google.com/document/d/11i14UI1cUz-OJ0knD5BFu7fmT6Fo327zvMYqfSAR7xs/edit#heading=h.qif6pkqyjgzn
var name: CharSequence? = null var name: CharSequence? = null
monarchy.doWithRealm { realm -> monarchy.doWithRealm { realm ->
val roomName = EventEntity.where(realm, room.roomId, EventType.STATE_ROOM_NAME).last()?.asDomain() val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).last()?.asDomain()
name = roomName?.content.toModel<RoomNameContent>()?.name name = roomName?.content.toModel<RoomNameContent>()?.name
if (!name.isNullOrEmpty()) { if (!name.isNullOrEmpty()) {
return@doWithRealm return@doWithRealm
} }


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


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


val roomMembers = RoomMembers(realm, room.roomId) val roomMembers = RoomMembers(realm, roomId)
val otherRoomMembers = roomMembers.getLoaded() val otherRoomMembers = roomMembers.getLoaded()
.filterKeys { it != credentials.userId } .filterKeys { it != credentials.userId }


if (room.myMembership == MyMembership.INVITED) { if (roomEntity?.membership == MyMembership.INVITED) {
//TODO handle invited //TODO handle invited
/* /*
if (currentUser != null if (currentUser != null
@ -94,7 +95,7 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
name = context.getString(R.string.room_displayname_room_invite) name = context.getString(R.string.room_displayname_room_invite)
} else { } else {


val roomSummary = RoomSummaryEntity.where(realm, room.roomId).findFirst() val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
val memberIds = if (roomSummary?.heroes?.isNotEmpty() == true) { val memberIds = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes roomSummary.heroes
} else { } else {
@ -125,6 +126,6 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
} }
return@doWithRealm return@doWithRealm
} }
return name ?: room.roomId return name ?: roomId
} }
} }

View File

@ -0,0 +1,54 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.room.read

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchCopied

internal class DefaultReadService(private val roomId: String,
private val monarchy: Monarchy,
private val setReadMarkersTask: SetReadMarkersTask,
private val taskExecutor: TaskExecutor) : ReadService {

override fun markAllAsRead(callback: MatrixCallback<Void>) {
val latestEvent = getLatestEvent()
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = latestEvent?.eventId, readReceiptEventId = latestEvent?.eventId)
setReadMarkersTask.configureWith(params).executeBy(taskExecutor)
}

override fun setReadReceipt(eventId: String, callback: MatrixCallback<Void>) {
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId)
setReadMarkersTask.configureWith(params).executeBy(taskExecutor)
}

override fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Void>) {
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = fullyReadEventId, readReceiptEventId = null)
setReadMarkersTask.configureWith(params).executeBy(taskExecutor)
}

private fun getLatestEvent(): EventEntity? {
return monarchy.fetchCopied { EventEntity.latestEvent(it, roomId) }
}


}

View File

@ -0,0 +1,101 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.room.read

import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.auth.data.Credentials
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.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.tryTransactionAsync

internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Unit> {

data class Params(
val roomId: String,
val fullyReadEventId: String?,
val readReceiptEventId: String?
)
}

private const val READ_MARKER = "m.fully_read"
private const val READ_RECEIPT = "m.read"

internal class DefaultSetReadMarkersTask(private val roomAPI: RoomAPI,
private val credentials: Credentials,
private val monarchy: Monarchy
) : SetReadMarkersTask {

override fun execute(params: SetReadMarkersTask.Params): Try<Unit> {
val markers = HashMap<String, String>()
if (params.fullyReadEventId != null && MatrixPatterns.isEventId(params.fullyReadEventId)) {
markers[READ_MARKER] = params.fullyReadEventId
}
if (params.readReceiptEventId != null
&& MatrixPatterns.isEventId(params.readReceiptEventId)
&& !isEventRead(params.roomId, params.readReceiptEventId)) {

updateNotificationCountIfNecessary(params.roomId, params.readReceiptEventId)
markers[READ_RECEIPT] = params.readReceiptEventId
}
return if (markers.isEmpty()) {
Try.just(Unit)
} else {
executeRequest {
apiCall = roomAPI.sendReadMarker(params.roomId, markers)
}
}
}

private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) {
monarchy.tryTransactionAsync { realm ->
val isLatestReceived = EventEntity.latestEvent(realm, eventId)?.eventId == eventId
if (isLatestReceived) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
?: return@tryTransactionAsync
roomSummary.notificationCount = 0
roomSummary.highlightCount = 0
}
}
}

private fun isEventRead(roomId: String, eventId: String): Boolean {
var isEventRead = false
monarchy.doWithRealm {
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
?: return@doWithRealm
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
?: return@doWithRealm
val readReceiptIndex = liveChunk.events.find(readReceipt.eventId)?.displayIndex
?: -1
val eventToCheckIndex = liveChunk.events.find(eventId)?.displayIndex ?: -1
isEventRead = eventToCheckIndex >= readReceiptIndex
}
return isEventRead
}

}

View File

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.MyMembership import im.vector.matrix.android.api.session.room.model.MyMembership
import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
import im.vector.matrix.android.internal.database.helper.addAll import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.addOrUpdate import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvents import im.vector.matrix.android.internal.database.helper.addStateEvents
@ -31,13 +32,19 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
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.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.session.sync.model.* 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.RoomSyncAccountData
import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject import io.realm.kotlin.createObject



internal class RoomSyncHandler(private val monarchy: Monarchy, internal class RoomSyncHandler(private val monarchy: Monarchy,
private val readReceiptHandler: ReadReceiptHandler) { private val readReceiptHandler: ReadReceiptHandler,
private val roomTagHandler: RoomTagHandler) {


sealed class HandlingStrategy { sealed class HandlingStrategy {
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy() data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
@ -105,9 +112,18 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
handleRoomSummary(realm, roomId, roomSync.summary) handleRoomSummary(realm, roomId, roomSync.summary)
} }


if (roomSync.unreadNotifications != null) {
handleUnreadNotifications(realm, roomId, roomSync.unreadNotifications)
}

if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) { if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
handleEphemeral(realm, roomId, roomSync.ephemeral) handleEphemeral(realm, roomId, roomSync.ephemeral)
} }

if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) {
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
}

return roomEntity return roomEntity
} }


@ -182,4 +198,26 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
.map { it.content.toModel<ReadReceiptContent>() } .map { it.content.toModel<ReadReceiptContent>() }
.flatMap { readReceiptHandler.handle(realm, roomId, it) } .flatMap { readReceiptHandler.handle(realm, roomId, it) }
} }

private fun handleUnreadNotifications(realm: Realm, roomId: String, unreadNotifications: RoomSyncUnreadNotifications) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: RoomSummaryEntity(roomId)

if (unreadNotifications.highlightCount != null) {
roomSummaryEntity.highlightCount = unreadNotifications.highlightCount
}
if (unreadNotifications.notificationCount != null) {
roomSummaryEntity.notificationCount = unreadNotifications.notificationCount
}
realm.insertOrUpdate(roomSummaryEntity)

}

private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
accountData.events
.filter { it.type == EventType.TAG }
.map { it.content.toModel<RoomTagContent>() }
.forEach { roomTagHandler.handle(realm, roomId, it) }
}

} }

View File

@ -0,0 +1,50 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.sync

import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomTagEntity
import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm
import java.util.*

internal class RoomTagHandler {

fun handle(realm: Realm, roomId: String, content: RoomTagContent?) {
if (content == null) {
return
}
val tags = ArrayList<RoomTagEntity>()
for (tagName in content.tags.keys) {
val params = content.tags[tagName]
val tag = if (params != null) {
RoomTagEntity(tagName, params["order"])
} else {
RoomTagEntity(tagName, null)
}
tags.add(tag)
}
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: RoomSummaryEntity(roomId)

roomSummaryEntity.tags.clear()
roomSummaryEntity.tags.addAll(tags)
realm.insertOrUpdate(roomSummaryEntity)
}

}

View File

@ -36,7 +36,11 @@ internal class SyncModule {
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
RoomSyncHandler(get(), get()) RoomTagHandler()
}

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


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {

View File

@ -25,5 +25,5 @@ internal data class RoomSyncAccountData(
/** /**
* List of account data events (array of Event). * List of account data events (array of Event).
*/ */
@Json(name = "events") val events: List<Event>? = null @Json(name = "events") val events: List<Event> = emptyList()
) )

View File

@ -19,6 +19,8 @@ package im.vector.matrix.android.internal.util
import arrow.core.Try import arrow.core.Try
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm import io.realm.Realm
import io.realm.RealmModel
import java.util.concurrent.atomic.AtomicReference


internal fun Monarchy.tryTransactionSync(transaction: (realm: Realm) -> Unit): Try<Unit> { internal fun Monarchy.tryTransactionSync(transaction: (realm: Realm) -> Unit): Try<Unit> {
return Try { return Try {
@ -31,3 +33,12 @@ internal fun Monarchy.tryTransactionAsync(transaction: (realm: Realm) -> Unit):
this.writeAsync(transaction) this.writeAsync(transaction)
} }
} }

fun <T : RealmModel> Monarchy.fetchCopied(query: (Realm) -> T?): T? {
val ref = AtomicReference<T>()
doWithRealm { realm ->
val result = query.invoke(realm)?.let { realm.copyFromRealm(it) }
ref.set(result)
}
return ref.get()
}