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
}

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

View File

@ -16,10 +16,11 @@

package im.vector.riotredesign.core.epoxy

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

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

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

abstract fun bind()

@ -47,6 +49,16 @@ abstract class KotlinModel(
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

protected fun <V : View> bind(@IdRes id: Int) = object : ReadOnlyProperty<KotlinModel, V> {
@ -56,7 +68,7 @@ abstract class KotlinModel(
// be optimized with a map
@Suppress("UNCHECKED_CAST")
return view?.findViewById(id) as V?
?: throw IllegalStateException("View ID $id for '${property.name}' not found.")
?: throw IllegalStateException("View ID $id for '${property.name}' not found.")
}
}
}

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.TimelineItemFactory
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

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

factory {
RoomSummaryController(get())
}

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

single {
RoomSummaryComparator()
}


}
}

View File

@ -16,9 +16,12 @@

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

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

sealed class RoomDetailActions {

data class SendMessage(val text: String) : 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 androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable
@ -75,7 +77,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {

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

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

private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView)
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
recyclerView.layoutManager = layoutManager
@ -100,7 +104,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
val textMessage = composerEditText.text.toString()
if (textMessage.isNotBlank()) {
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)
}

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

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

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

companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {

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

fun accept(action: RoomDetailActions) {
fun process(action: RoomDetailActions) {
when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.IsDisplayed -> visibleRoomHolder.setVisibleRoom(roomId)
is RoomDetailActions.SendMessage -> handleSendMessage(action)
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> {})
}

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() {
room.rx().liveRoomSummary()
.execute { async ->

View File

@ -16,12 +16,16 @@

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

import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyAsyncUtil
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.room.timeline.TimelineEvent
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.features.home.LoadingItemModel_
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 {
it.id(event.localId)
it.setOnVisibilityStateChanged(OnModelVisibilityStateChangedListener<KotlinModel, View> { model, view, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {
callback?.onEventVisible(event, currentPosition)
}
})
epoxyModels.add(it)
}
if (addDaySeparator) {
@ -98,6 +107,7 @@ class TimelineEventController(private val roomId: String,


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


View File

@ -16,9 +16,9 @@

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.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.KotlinModel

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

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

return when (event.root.type) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)
EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event)
EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event)
EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event)
else -> defaultItemFactory.create(event)
else -> defaultItemFactory.create(event)
}
}


View File

@ -26,9 +26,12 @@ import im.vector.riotredesign.core.epoxy.KotlinModel
data class RoomCategoryItem(
val title: CharSequence,
val isExpanded: Boolean,
val unreadCount: Int,
val showHighlighted: Boolean,
val listener: (() -> Unit)? = null
) : 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 rootView by bind<ViewGroup>(R.id.roomCategoryRootView)

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

View File

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

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

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

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
roomController = RoomSummaryController(this)
roomController.callback = this
stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(roomController)
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.group.model.GroupSummary
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.riotredesign.core.platform.RiotViewModel
import im.vector.riotredesign.features.home.group.SelectedGroupHolder
@ -32,6 +33,7 @@ import io.reactivex.Observable
import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit

typealias RoomListFilterName = CharSequence

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

companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> {
@ -50,7 +53,8 @@ class RoomListViewModel(initialState: RoomListViewState,
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupHolder>()
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() {
Observable.combineLatest<List<RoomSummary>, Option<GroupSummary>, Option<RoomListFilterName>, RoomSummaries>(
session.rx().liveRoomSummaries(),
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
selectedGroupHolder.selectedGroup(),
roomListFilter,
roomListFilter.throttleLast(300, TimeUnit.MILLISECONDS),
Function3 { rooms, selectedGroupOption, filterRoomOption ->
val filterRoom = filterRoomOption.orNull()
val filteredRooms = rooms.filter {
if (filterRoom.isNullOrBlank()) {
true
} else {
it.displayName.contains(other = filterRoom, ignoreCase = true)
}
}

val filteredRooms = filterRooms(rooms, filterRoomOption)
val selectedGroup = selectedGroupOption.orNull()
val filteredDirectRooms = filteredRooms
.filter { it.isDirect }
@ -123,7 +119,7 @@ class RoomListViewModel(initialState: RoomListViewState,
.filter {
selectedGroup?.roomIds?.contains(it.roomId) ?: true
}
RoomSummaries(filteredDirectRooms, filteredGroupRooms)
buildRoomSummaries(filteredDirectRooms + filteredGroupRooms)
}
)
.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

data class RoomSummaries(
val favourites: 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 {
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

import androidx.annotation.StringRes
import com.airbnb.epoxy.TypedEpoxyController
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>() {

private var isDirectRoomsExpanded = true
private var isGroupRoomsExpanded = true
private var isFavoriteRoomsExpanded = 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) {
val roomSummaries = viewState.asyncRooms()
RoomCategoryItem(
title = "DIRECT MESSAGES",
isExpanded = isDirectRoomsExpanded,
listener = {
isDirectRoomsExpanded = !isDirectRoomsExpanded
setData(viewState)
}
)
.id("direct_messages")
.addTo(this)

if (isDirectRoomsExpanded) {
buildRoomModels(roomSummaries?.directRooms ?: emptyList(), viewState.selectedRoomId)
val favourites = roomSummaries?.favourites ?: emptyList()
buildRoomCategory(viewState, favourites, R.string.room_list_favourites, isFavoriteRoomsExpanded) {
isFavoriteRoomsExpanded = !isFavoriteRoomsExpanded
}
if (isFavoriteRoomsExpanded) {
buildRoomModels(favourites, viewState.selectedRoomId)
}

RoomCategoryItem(
title = "GROUPS",
isExpanded = isGroupRoomsExpanded,
listener = {
isGroupRoomsExpanded = !isGroupRoomsExpanded
setData(viewState)
}
)
.id("group_messages")
.addTo(this)
val directRooms = roomSummaries?.directRooms ?: emptyList()
buildRoomCategory(viewState, directRooms, R.string.room_list_direct, isDirectRoomsExpanded) {
isDirectRoomsExpanded = !isDirectRoomsExpanded
}
if (isDirectRoomsExpanded) {
buildRoomModels(directRooms, viewState.selectedRoomId)
}

val groupRooms = roomSummaries?.groupRooms ?: emptyList()
buildRoomCategory(viewState, groupRooms, R.string.room_list_group, isGroupRoomsExpanded) {
isGroupRoomsExpanded = !isGroupRoomsExpanded
}
if (isGroupRoomsExpanded) {
buildRoomModels(roomSummaries?.groupRooms ?: emptyList(), viewState.selectedRoomId)
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)
}
)
.id(titleRes)
.addTo(this)
}

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

View File

@ -14,24 +14,22 @@
* 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
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.session.room.DefaultRoom
object RoomSummaryFormatter {


internal object RoomMapper {


fun map(roomEntity: RoomEntity): Room {
return DefaultRoom(
roomEntity.roomId,
roomEntity.membership
)
/**
* Format the unread messages counter.
*
* @param count the count
* @return the formatted value
*/
fun formatUnreadMessagesCounter(count: Int): String {
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 avatarUrl: String?,
val isSelected: Boolean,
val unreadCount: Int,
val showHighlighted: Boolean,
val listener: (() -> Unit)? = null
) : 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 avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
private val rootView by bind<CheckableFrameLayout>(R.id.itemRoomLayout)

override fun bind() {
unreadCounterBadgeView.render(unreadCount, showHighlighted)
rootView.isChecked = isSelected
rootView.setOnClickListener { listener?.invoke() }
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"?>

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

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

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

View File

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

@ -26,16 +26,32 @@
android:text="DIRECT MESSAGES"
android:textColor="@color/bluey_grey_two"
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_constraintStart_toStartOf="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
android:id="@+id/roomCategoryAddButton"
android:layout_width="32dp"
android:layout_height="32dp"

android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="centerInside"
android:src="@drawable/ic_add_circle_white"
android:tint="@color/bluey_grey_two"

View File

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

View File

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

<string name="global_retry">Réessayer</string>
<string name="error_no_network">Pas de connexion internet</string>
<string name="error_common">Une erreur est survenue</string>
<string name="room_list_empty">Rejoignez une room pour commencer à utiliser l\'application</string>
<string name="global_retry">"Retry"</string>
<string name="error_no_network">"No network connection"</string>
<string name="error_common">"An error occurred"</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>

View File

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

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

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

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

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


View File

@ -27,7 +27,7 @@ interface RoomService {
/**
* Get a room from a roomId
* @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?


View File

@ -16,6 +16,9 @@

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.
* 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 avatarUrl: String = "",
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

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


internal object RoomSummaryMapper {

fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
val tags = roomSummaryEntity.tags.map {
RoomTag(it.tagName, it.tagOrder)
}
return RoomSummary(
roomSummaryEntity.roomId,
roomSummaryEntity.displayName ?: "",
roomSummaryEntity.topic ?: "",
roomSummaryEntity.avatarUrl ?: "",
roomSummaryEntity.isDirect,
roomSummaryEntity.otherMemberIds.toList()
roomId = roomSummaryEntity.roomId,
displayName = roomSummaryEntity.displayName ?: "",
topic = roomSummaryEntity.topic ?: "",
avatarUrl = roomSummaryEntity.avatarUrl ?: "",
isDirect = roomSummaryEntity.isDirect,
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 invitedMembersCount: Int? = 0,
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() {

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

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


internal fun RealmQuery<EventEntity>.last(since: Int? = null, strict: Boolean = false): EventEntity? {
if (since != null) {
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.NetworkConnectivityChecker
import im.vector.matrix.android.internal.network.UnitConverterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import okreplay.OkReplayInterceptor
import org.koin.dsl.module.module
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import timber.log.Timber
@ -62,10 +62,6 @@ class NetworkModule {
MoshiProvider.providesMoshi()
}

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

single {
NetworkConnectivityChecker(get())
}
@ -73,7 +69,8 @@ class NetworkModule {
factory {
Retrofit.Builder()
.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.room.DefaultRoomService
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.members.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayNameResolver
@ -46,6 +47,10 @@ internal class SessionModule(private val sessionParams: SessionParams) {
sessionParams
}

scope(DefaultSession.SCOPE) {
sessionParams.credentials
}

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

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


scope(DefaultSession.SCOPE) {
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.Transformations
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.send.SendService
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.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.util.Cancelable
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.RoomSummaryEntityFields
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.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import org.koin.core.parameter.parametersOf
import org.koin.standalone.inject

internal data class DefaultRoom(
override val roomId: String,
override val myMembership: MyMembership
) : Room, MatrixKoinComponent {
private val loadRoomMembersTask: LoadRoomMembersTask,
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>()
private val timelineService by inject<TimelineService> { parametersOf(roomId) }
private val sendService by inject<SendService> { parametersOf(roomId) }
private val taskExecutor by inject<TaskExecutor>()

) : Room,
TimelineService by timelineService,
SendService by sendService,
ReadService by readService {

override val roomSummary: LiveData<RoomSummary> by lazy {
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 {
val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE)
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.RoomSummaryEntityFields
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? {
var room: Room? = null
monarchy.doWithRealm { realm ->
room = RoomEntity.where(realm, roomId).findFirst()?.asDomain()
}
return room
monarchy.fetchCopied { RoomEntity.where(it, roomId).findFirst() } ?: return null
return roomFactory.instantiate(roomId)
}

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.PaginationResponse
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 {

@ -100,5 +105,14 @@ internal interface RoomAPI {
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/event/{eventId}")
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.session.events.model.EventType
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.RoomAvatarContent
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.last
import im.vector.matrix.android.internal.database.query.where
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
*
* @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
*/
fun resolve(room: Room): String? {
fun resolve(roomId: String): String? {
var res: String? = null
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
if (!res.isNullOrEmpty()) {
return@doWithRealm
}
val roomMembers = RoomMembers(realm, room.roomId)
val roomMembers = RoomMembers(realm, roomId)
val members = roomMembers.getLoaded()
if (room.myMembership == MyMembership.INVITED) {
if (roomEntity?.membership == MyMembership.INVITED) {
if (members.size == 1) {
res = members.entries.first().value.avatarUrl
} 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

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.room.members.DefaultLoadRoomMembersTask
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.send.DefaultSendService
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
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.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.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.util.PagingRequestHelper
import org.koin.dsl.module.module
import retrofit2.Retrofit
import java.util.concurrent.Executors


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

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

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

factory { (roomId: String) ->
DefaultSendService(roomId, get(), get()) as SendService
scope(DefaultSession.SCOPE) {
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.session.events.model.EventType
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.internal.database.RealmLiveEntityObserver
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.RoomSummaryEntity
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.session.room.members.RoomDisplayNameResolver
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 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 ->
rooms.forEach { updateRoom(realm, it) }
}
}

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

val lastMessageEvent = EventEntity.where(realm, room.roomId, EventType.MESSAGE).last()
val lastTopicEvent = EventEntity.where(realm, room.roomId, EventType.STATE_ROOM_TOPIC).last()?.asDomain()
val lastEvent = EventEntity.latestEvent(realm, roomId, includedTypes = listOf(EventType.MESSAGE))
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.avatarUrl = roomAvatarResolver.resolve(room)
roomSummary.displayName = roomDisplayNameResolver.resolve(context, roomId).toString()
roomSummary.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummary.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
roomSummary.lastMessage = lastMessageEvent
roomSummary.lastMessage = lastEvent
roomSummary.otherMemberIds.clear()
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.session.events.model.EventType
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.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.last
import im.vector.matrix.android.internal.database.query.where
@ -45,10 +45,10 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
* Compute the room display name
*
* @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
*/
fun resolve(context: Context, room: Room): CharSequence {
fun resolve(context: Context, roomId: String): CharSequence {
// this algorithm is the one defined in
// https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617
// calculateRoomName(room, userId)
@ -57,29 +57,30 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
// https://docs.google.com/document/d/11i14UI1cUz-OJ0knD5BFu7fmT6Fo327zvMYqfSAR7xs/edit#heading=h.qif6pkqyjgzn
var name: CharSequence? = null
monarchy.doWithRealm { realm ->
val roomName = EventEntity.where(realm, room.roomId, EventType.STATE_ROOM_NAME).last()?.asDomain()
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
if (!name.isNullOrEmpty()) {
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
if (!name.isNullOrEmpty()) {
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()
if (!name.isNullOrEmpty()) {
return@doWithRealm
}

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

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

val roomSummary = RoomSummaryEntity.where(realm, room.roomId).findFirst()
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
val memberIds = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes
} else {
@ -125,6 +126,6 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
}
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.toModel
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.addOrUpdate
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.where
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.kotlin.createObject


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

sealed class HandlingStrategy {
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
@ -57,9 +64,9 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,

private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy) {
val rooms = when (handlingStrategy) {
is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) }
is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) }
is HandlingStrategy.INVITED -> handlingStrategy.data.map { handleInvitedRoom(realm, it.key, it.value) }
is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftRoom(it.key, it.value) }
is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftRoom(it.key, it.value) }
}
realm.insertOrUpdate(rooms)
}
@ -69,7 +76,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
roomSync: RoomSync): RoomEntity {

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

if (roomEntity.membership == MyMembership.INVITED) {
roomEntity.chunks.deleteAllFromRealm()
@ -105,9 +112,18 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
handleRoomSummary(realm, roomId, roomSync.summary)
}

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

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

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

return roomEntity
}

@ -159,7 +175,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
roomSummary: RoomSyncSummary) {

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

if (roomSummary.heroes.isNotEmpty()) {
roomSummaryEntity.heroes.clear()
@ -182,4 +198,26 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
.map { it.content.toModel<ReadReceiptContent>() }
.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) {
RoomSyncHandler(get(), get())
RoomTagHandler()
}

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

scope(DefaultSession.SCOPE) {

View File

@ -25,5 +25,5 @@ internal data class RoomSyncAccountData(
/**
* 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 com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmModel
import java.util.concurrent.atomic.AtomicReference

internal fun Monarchy.tryTransactionSync(transaction: (realm: Realm) -> Unit): Try<Unit> {
return Try {
@ -30,4 +32,13 @@ internal fun Monarchy.tryTransactionAsync(transaction: (realm: Realm) -> Unit):
return Try {
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()
}