diff --git a/app/build.gradle b/app/build.gradle index bc195796..1271c1ea 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 { diff --git a/app/src/main/java/im/vector/riotredesign/core/epoxy/KotlinModel.kt b/app/src/main/java/im/vector/riotredesign/core/epoxy/KotlinModel.kt index ef4dd32b..dd651468 100644 --- a/app/src/main/java/im/vector/riotredesign/core/epoxy/KotlinModel.kt +++ b/app/src/main/java/im/vector/riotredesign/core/epoxy/KotlinModel.kt @@ -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? = 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 { + this.onModelVisibilityStateChangedListener = listener + return this + } + override fun getDefaultLayout() = layoutRes protected fun bind(@IdRes id: Int) = object : ReadOnlyProperty { @@ -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.") } } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/extensions/Iterable.kt b/app/src/main/java/im/vector/riotredesign/core/extensions/Iterable.kt new file mode 100644 index 00000000..092a94ac --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/core/extensions/Iterable.kt @@ -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 > Iterable.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 +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index ae703990..3b3bd51d 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -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() + } + } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 7946e89c..f3ca3b3d 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 80d56cbf..76f91c7b 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -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)) + } + } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index 537a23f8..3d1d4cf1 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -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() + companion object : MvRxViewModelFactory { @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 {}) } + 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 {}) + } + }) + .disposeOnClear() + } + private fun observeRoomSummary() { room.rx().liveRoomSummary() .execute { async -> diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 12dcb895..8fc268c3 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -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 { 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) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineItemFactory.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineItemFactory.kt index 8c38e2ec..cc30b010 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineItemFactory.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineItemFactory.kt @@ -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) } } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomCategoryItem.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomCategoryItem.kt index c5d3044d..7fcc0210 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomCategoryItem.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomCategoryItem.kt @@ -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(R.id.roomCategoryUnreadCounterBadgeView) private val titleView by bind(R.id.roomCategoryTitleView) private val rootView by bind(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() } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt index 8aa095cd..904cf66c 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt @@ -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() + private val roomController by inject() 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() diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt index 5028bc5d..68d41bae 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt @@ -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(initialState) { companion object : MvRxViewModelFactory { @@ -50,7 +53,8 @@ class RoomListViewModel(initialState: RoomListViewState, val roomSelectionRepository = viewModelContext.activity.get() val selectedGroupHolder = viewModelContext.activity.get() val visibleRoomHolder = viewModelContext.activity.get() - return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository) + val roomSummaryComparator = viewModelContext.activity.get() + return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository, roomSummaryComparator) } } @@ -92,19 +96,11 @@ class RoomListViewModel(initialState: RoomListViewState, private fun observeRoomSummaries() { Observable.combineLatest, Option, Option, 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, filterRoomOption: Option): List { + val filterRoom = filterRoomOption.orNull() + return rooms.filter { + if (filterRoom.isNullOrBlank()) { + true + } else { + it.displayName.contains(other = filterRoom, ignoreCase = true) + } + } + } + + private fun buildRoomSummaries(rooms: List): RoomSummaries { + val favourites = ArrayList() + val directChats = ArrayList() + val groupRooms = ArrayList() + val lowPriorities = ArrayList() + val serverNotices = ArrayList() + + 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) + ) + } + + } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt index 5b6d6f25..57d0be76 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt @@ -27,10 +27,13 @@ data class RoomListViewState( ) : MvRxState data class RoomSummaries( + val favourites: List, val directRooms: List, - val groupRooms: List + val groupRooms: List, + val lowPriorities: List, + val serverNotices: List ) fun RoomSummaries?.isNullOrEmpty(): Boolean { - return this == null || (directRooms.isEmpty() && groupRooms.isEmpty()) + return this == null || (directRooms.isEmpty() && groupRooms.isEmpty() && favourites.isEmpty() && lowPriorities.isEmpty() && serverNotices.isEmpty()) } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryComparator.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryComparator.kt new file mode 100644 index 00000000..8d92b694 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryComparator.kt @@ -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 { + + 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 + + } + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt index 1c618174..cc69d375 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt @@ -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() { - 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, @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, 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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMapper.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryFormatter.kt similarity index 54% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMapper.kt rename to app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryFormatter.kt index bfedb78d..4666d99b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMapper.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryFormatter.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt index 3949f323..6b4bd8a3 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt @@ -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(R.id.roomUnreadCounterBadgeView) private val titleView by bind(R.id.roomNameView) private val avatarImageView by bind(R.id.roomAvatarImageView) private val rootView by bind(R.id.itemRoomLayout) override fun bind() { + unreadCounterBadgeView.render(unreadCount, showHighlighted) rootView.isChecked = isSelected rootView.setOnClickListener { listener?.invoke() } titleView.text = roomName diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/UnreadCounterBadgeView.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/UnreadCounterBadgeView.kt new file mode 100755 index 00000000..44de73e8 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/UnreadCounterBadgeView.kt @@ -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 + } + +} diff --git a/app/src/main/res/drawable/bg_unread_highlight.xml b/app/src/main/res/drawable/bg_unread_highlight.xml new file mode 100644 index 00000000..371c1e7d --- /dev/null +++ b/app/src/main/res/drawable/bg_unread_highlight.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_unread_notification.xml b/app/src/main/res/drawable/bg_unread_notification.xml new file mode 100644 index 00000000..496134a7 --- /dev/null +++ b/app/src/main/res/drawable/bg_unread_notification.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_room.xml b/app/src/main/res/layout/item_room.xml index 93540f7e..c07164d1 100644 --- a/app/src/main/res/layout/item_room.xml +++ b/app/src/main/res/layout/item_room.xml @@ -1,18 +1,17 @@ - + android:focusable="true" + android:foreground="?attr/selectableItemBackground" + android:paddingLeft="8dp" + android:paddingRight="16dp"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_room_category.xml b/app/src/main/res/layout/item_room_category.xml index f3e1c54a..f84f9c42 100644 --- a/app/src/main/res/layout/item_room_category.xml +++ b/app/src/main/res/layout/item_room_category.xml @@ -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" /> + + #a5aab2 #ebedf8 #a5a5a5 + #61708B diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f51599c2..62328533 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,9 +1,15 @@ - Riot X + "Riot X" - Réessayer - Pas de connexion internet - Une erreur est survenue - Rejoignez une room pour commencer à utiliser l\'application + "Retry" + "No network connection" + "An error occurred" + + "Join a room to start using the app." + "Favourites" + "People" + "Rooms" + "Low priority" + "System Alerts" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index bcb1e9dc..20ec0d53 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index 85f1c2f8..79b514fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -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? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index 0759086b..bda5a54c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -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 = emptyList() + val lastMessage: Event? = null, + val otherMemberIds: List = emptyList(), + var notificationCount: Int = 0, + var highlightCount: Int = 0, + var tags: List = emptyList() ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tag/RoomTag.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tag/RoomTag.kt new file mode 100644 index 00000000..0623e8d9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tag/RoomTag.kt @@ -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" + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tag/RoomTagContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tag/RoomTagContent.kt new file mode 100644 index 00000000..a5f7650b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tag/RoomTagContent.kt @@ -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> = emptyMap() +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt new file mode 100644 index 00000000..b7b78bc8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt @@ -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) + + /** + * Set the read receipt on the event with provided eventId. + */ + fun setReadReceipt(eventId: String, callback: MatrixCallback) + + /** + * Set the read marker on the event with provided eventId. + */ + fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 550943a7..9243d1de 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -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 ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 79524026..9f9c66f0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -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 = RealmList() + var otherMemberIds: RealmList = RealmList(), + var notificationCount: Int = 0, + var highlightCount: Int = 0, + var tags: RealmList = RealmList() ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomTagEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomTagEntity.kt new file mode 100644 index 00000000..12ca29b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomTagEntity.kt @@ -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 + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index 48bcd0b3..015a47ac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -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 = emptyList(), + excludedTypes: List = 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.next(from: Int? = null, strict: Boolean = true): EventEntity? { if (from != null) { @@ -62,7 +78,6 @@ internal fun RealmQuery.next(from: Int? = null, strict: Boolean = t .findFirst() } - internal fun RealmQuery.last(since: Int? = null, strict: Boolean = false): EventEntity? { if (since != null) { if (strict) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt index 97037a92..d9abf791 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt @@ -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())) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UnitConverterFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UnitConverterFactory.kt new file mode 100644 index 00000000..74d9623a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UnitConverterFactory.kt @@ -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, + retrofit: Retrofit): Converter? { + return if (type == Unit::class.java) UnitConverter else null + } + + private object UnitConverter : Converter { + override fun convert(value: ResponseBody) { + value.close() + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 460a899b..91da7dd5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -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() 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 } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 9a0bad45..e241a9f6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -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() - private val monarchy by inject() - private val timelineService by inject { parametersOf(roomId) } - private val sendService by inject { parametersOf(roomId) } - private val taskExecutor by inject() + +) : Room, + TimelineService by timelineService, + SendService by sendService, + ReadService by readService { override val roomSummary: LiveData by lazy { val liveData = monarchy @@ -62,19 +60,8 @@ internal data class DefaultRoom( } } - override fun timeline(eventId: String?): LiveData { - 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): Cancelable { - return sendService.sendTextMessage(text, callback) - } - - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 85f9c0e4..b1462789 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -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> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index b14b0fcf..ecb4d42d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -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 + /** + * 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): Call + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt index ee2b3613..235d18e7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt @@ -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()?.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) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt new file mode 100644 index 00000000..6b2c796c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -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 + ) + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 3ebaa0ee..716fd155 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -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() - 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()) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index f414847a..4d690832 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -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.where(it) } override fun process(inserted: List, updated: List, deleted: List) { - 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()?.topic - roomSummary.lastMessage = lastMessageEvent + roomSummary.lastMessage = lastEvent roomSummary.otherMemberIds.clear() roomSummary.otherMemberIds.addAll(otherRoomMembers.keys) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt index 48cd8278..7eb0f85f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt @@ -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()?.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()?.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()?.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 } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt new file mode 100644 index 00000000..467e6117 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -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) { + 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) { + val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId) + setReadMarkersTask.configureWith(params).executeBy(taskExecutor) + } + + override fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) { + 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) } + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt new file mode 100644 index 00000000..f127966d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -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 { + + 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 { + val markers = HashMap() + 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 + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index ed1a7d5e..7cd703c7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -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) : 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() } .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() } + .forEach { roomTagHandler.handle(realm, roomId, it) } + } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomTagHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomTagHandler.kt new file mode 100644 index 00000000..c3722ea6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomTagHandler.kt @@ -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() + 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) + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt index 9a296607..c62bc2e1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt @@ -36,7 +36,11 @@ internal class SyncModule { } scope(DefaultSession.SCOPE) { - RoomSyncHandler(get(), get()) + RoomTagHandler() + } + + scope(DefaultSession.SCOPE) { + RoomSyncHandler(get(), get(), get()) } scope(DefaultSession.SCOPE) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/RoomSyncAccountData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/RoomSyncAccountData.kt index 07b323b2..6c2e79f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/RoomSyncAccountData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/RoomSyncAccountData.kt @@ -25,5 +25,5 @@ internal data class RoomSyncAccountData( /** * List of account data events (array of Event). */ - @Json(name = "events") val events: List? = null + @Json(name = "events") val events: List = emptyList() ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt index 24813320..809ba16b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt @@ -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 { return Try { @@ -30,4 +32,13 @@ internal fun Monarchy.tryTransactionAsync(transaction: (realm: Realm) -> Unit): return Try { this.writeAsync(transaction) } +} + +fun Monarchy.fetchCopied(query: (Realm) -> T?): T? { + val ref = AtomicReference() + doWithRealm { realm -> + val result = query.invoke(realm)?.let { realm.copyFromRealm(it) } + ref.set(result) + } + return ref.get() } \ No newline at end of file