Merge branch 'develop' into feature/read_marker

This commit is contained in:
ganfra 2019-10-01 13:37:21 +02:00
commit 42e0a45f3f
14 changed files with 138 additions and 86 deletions

View File

@ -16,6 +16,7 @@ Bugfix:
- Fix issue on upload error in loop (#587) - Fix issue on upload error in loop (#587)
- Fix opening a permalink: the targeted event is displayed twice (#556) - Fix opening a permalink: the targeted event is displayed twice (#556)
- Fix opening a permalink paginates all the history up to the last event (#282) - Fix opening a permalink paginates all the history up to the last event (#282)
- after login, the icon in the top left is a green 'A' for (all communities) rather than my avatar (#267)
Translations: Translations:
- -

View File

@ -20,7 +20,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.android.MainThreadDisposable import io.reactivex.android.MainThreadDisposable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
private class LiveDataObservable<T>( private class LiveDataObservable<T>(

View File

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.Optional
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
@ -45,6 +46,10 @@ class RxSession(private val session: Session) {
return session.livePushers().asObservable() return session.livePushers().asObservable()
} }
fun liveUser(userId: String): Observable<Optional<User>> {
return session.liveUser(userId).asObservable().distinctUntilChanged()
}
fun liveUsers(): Observable<List<User>> { fun liveUsers(): Observable<List<User>> {
return session.liveUsers().asObservable() return session.liveUsers().asObservable()
} }

View File

@ -21,6 +21,7 @@ import androidx.paging.PagedList
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
/** /**
* This interface defines methods to get users. It's implemented at the session level. * This interface defines methods to get users. It's implemented at the session level.
@ -47,9 +48,9 @@ interface UserService {
/** /**
* Observe a live user from a userId * Observe a live user from a userId
* @param userId the userId to look for. * @param userId the userId to look for.
* @return a Livedata of user with userId * @return a LiveData of user with userId
*/ */
fun liveUser(userId: String): LiveData<User?> fun liveUser(userId: String): LiveData<Optional<User>>
/** /**
* Observe a live list of users sorted alphabetically * Observe a live list of users sorted alphabetically

View File

@ -37,4 +37,6 @@ data class Optional<T : Any> constructor(private val value: T?) {
} }
} }
} }
fun <T : Any> T?.toOptional() = Optional(this)

View File

@ -26,6 +26,8 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.model.UserEntity
@ -66,7 +68,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
return userEntity.asDomain() return userEntity.asDomain()
} }
override fun liveUser(userId: String): LiveData<User?> { override fun liveUser(userId: String): LiveData<Optional<User>> {
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
UserEntity.where(realm, userId) UserEntity.where(realm, userId)
} }
@ -74,6 +76,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
results results
.map { it.asDomain() } .map { it.asDomain() }
.firstOrNull() .firstOrNull()
.toOptional()
} }
} }

View File

@ -20,19 +20,31 @@ import com.jakewharton.rxrelay2.BehaviorRelay
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
open class RxStore<T>(defaultValue: T? = null) { open class RxStore<T>(private val defaultValue: T? = null) {
private val storeSubject: BehaviorRelay<T> = if (defaultValue == null) { var storeRelay = createRelay()
BehaviorRelay.create<T>()
} else { fun clear() {
BehaviorRelay.createDefault(defaultValue) storeRelay = createRelay()
}
fun get(): T? {
return storeRelay.value
} }
fun observe(): Observable<T> { fun observe(): Observable<T> {
return storeSubject.hide().observeOn(Schedulers.computation()) return storeRelay.hide().observeOn(Schedulers.computation())
} }
fun post(value: T) { fun post(value: T) {
storeSubject.accept(value) storeRelay.accept(value)
}
private fun createRelay(): BehaviorRelay<T> {
return if (defaultValue == null) {
BehaviorRelay.create<T>()
} else {
BehaviorRelay.createDefault(defaultValue)
}
} }
} }

View File

@ -98,7 +98,8 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
selectedGroupStore.clear()
homeRoomListStore.clear()
session.removeListener(this) session.removeListener(this)
} }

View File

@ -17,18 +17,17 @@
package im.vector.riotx.features.home package im.vector.riotx.features.home
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.core.view.forEachIndexed import androidx.core.view.forEachIndexed
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.ToolbarConfigurable
@ -38,26 +37,17 @@ import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.home.room.list.RoomListParams import im.vector.riotx.features.home.room.list.RoomListParams
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
import im.vector.riotx.features.workers.signout.SignOutViewModel import im.vector.riotx.features.workers.signout.SignOutViewModel
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_home_detail.* import kotlinx.android.synthetic.main.fragment_home_detail.*
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@Parcelize
data class HomeDetailParams(
val groupId: String,
val groupName: String,
val groupAvatar: String
) : Parcelable
private const val INDEX_CATCHUP = 0 private const val INDEX_CATCHUP = 0
private const val INDEX_PEOPLE = 1 private const val INDEX_PEOPLE = 1
private const val INDEX_ROOMS = 2 private const val INDEX_ROOMS = 2
class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
private val params: HomeDetailParams by args()
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>() private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
private val viewModel: HomeDetailViewModel by fragmentViewModel() private val viewModel: HomeDetailViewModel by fragmentViewModel()
@ -84,11 +74,25 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
setupToolbar() setupToolbar()
setupKeysBackupBanner() setupKeysBackupBanner()
viewModel.selectSubscribe(this, HomeDetailViewState::groupSummary) { groupSummary ->
onGroupChange(groupSummary.orNull())
}
viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode -> viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode ->
switchDisplayMode(displayMode) switchDisplayMode(displayMode)
} }
} }
private fun onGroupChange(groupSummary: GroupSummary?) {
groupSummary?.let {
avatarRenderer.render(
it.avatarUrl,
it.groupId,
it.displayName,
groupToolbarAvatarImageView
)
}
}
private fun setupKeysBackupBanner() { private fun setupKeysBackupBanner() {
// Keys backup banner // Keys backup banner
// Use the SignOutViewModel, it observe the keys backup state and this is what we need here // Use the SignOutViewModel, it observe the keys backup state and this is what we need here
@ -130,12 +134,6 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
parentActivity.configure(groupToolbar) parentActivity.configure(groupToolbar)
} }
groupToolbar.title = "" groupToolbar.title = ""
avatarRenderer.render(
params.groupAvatar,
params.groupId,
params.groupName,
groupToolbarAvatarImageView
)
groupToolbarAvatarImageView.setOnClickListener { groupToolbarAvatarImageView.setOnClickListener {
navigationViewModel.goTo(HomeActivity.Navigation.OpenDrawer) navigationViewModel.goTo(HomeActivity.Navigation.OpenDrawer)
} }
@ -199,6 +197,7 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
} }
override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
Timber.v(it.toString())
unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup)) unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup))
unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople)) unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople))
unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms)) unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
@ -207,10 +206,8 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
companion object { companion object {
fun newInstance(args: HomeDetailParams): HomeDetailFragment { fun newInstance(): HomeDetailFragment {
return HomeDetailFragment().apply { return HomeDetailFragment()
setArguments(args)
}
} }
} }

View File

@ -25,6 +25,8 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.group.SelectedGroupStore
import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.ui.UiStateRepository import im.vector.riotx.features.ui.UiStateRepository
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
@ -36,7 +38,9 @@ import io.reactivex.schedulers.Schedulers
class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState, class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState,
private val session: Session, private val session: Session,
private val uiStateRepository: UiStateRepository, private val uiStateRepository: UiStateRepository,
private val homeRoomListStore: HomeRoomListObservableStore) private val selectedGroupStore: SelectedGroupStore,
private val homeRoomListStore: HomeRoomListObservableStore,
private val stringProvider: StringProvider)
: VectorViewModel<HomeDetailViewState>(initialState) { : VectorViewModel<HomeDetailViewState>(initialState) {
@AssistedInject.Factory @AssistedInject.Factory
@ -62,6 +66,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
init { init {
observeSyncState() observeSyncState()
observeSelectedGroupStore()
observeRoomSummaries() observeRoomSummaries()
} }
@ -88,42 +93,48 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
.disposeOnClear() .disposeOnClear()
} }
private fun observeSelectedGroupStore() {
selectedGroupStore
.observe()
.subscribe {
setState {
copy(groupSummary = it)
}
}
.disposeOnClear()
}
private fun observeRoomSummaries() { private fun observeRoomSummaries() {
homeRoomListStore homeRoomListStore
.observe() .observe()
.observeOn(Schedulers.computation()) .observeOn(Schedulers.computation())
.subscribe { list -> .map { it.asSequence() }
list.let { summaries -> .subscribe { summaries ->
val peopleNotifications = summaries val peopleNotifications = summaries
.filter { it.isDirect } .filter { it.isDirect }
.map { it.notificationCount } .map { it.notificationCount }
.takeIf { it.isNotEmpty() } .sumBy { i -> i }
?.sumBy { i -> i } val peopleHasHighlight = summaries
?: 0 .filter { it.isDirect }
val peopleHasHighlight = summaries .any { it.highlightCount > 0 }
.filter { it.isDirect }
.any { it.highlightCount > 0 }
val roomsNotifications = summaries val roomsNotifications = summaries
.filter { !it.isDirect } .filter { !it.isDirect }
.map { it.notificationCount } .map { it.notificationCount }
.takeIf { it.isNotEmpty() } .sumBy { i -> i }
?.sumBy { i -> i } val roomsHasHighlight = summaries
?: 0 .filter { !it.isDirect }
val roomsHasHighlight = summaries .any { it.highlightCount > 0 }
.filter { !it.isDirect }
.any { it.highlightCount > 0 }
setState { setState {
copy( copy(
notificationCountCatchup = peopleNotifications + roomsNotifications, notificationCountCatchup = peopleNotifications + roomsNotifications,
notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight, notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight,
notificationCountPeople = peopleNotifications, notificationCountPeople = peopleNotifications,
notificationHighlightPeople = peopleHasHighlight, notificationHighlightPeople = peopleHasHighlight,
notificationCountRooms = roomsNotifications, notificationCountRooms = roomsNotifications,
notificationHighlightRooms = roomsHasHighlight notificationHighlightRooms = roomsHasHighlight
) )
}
} }
} }
.disposeOnClear() .disposeOnClear()

View File

@ -16,11 +16,14 @@
package im.vector.riotx.features.home package im.vector.riotx.features.home
import arrow.core.Option
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListFragment
data class HomeDetailViewState( data class HomeDetailViewState(
val groupSummary: Option<GroupSummary> = Option.empty(),
val displayMode: RoomListFragment.DisplayMode = RoomListFragment.DisplayMode.HOME, val displayMode: RoomListFragment.DisplayMode = RoomListFragment.DisplayMode.HOME,
val notificationCountCatchup: Int = 0, val notificationCountCatchup: Int = 0,
val notificationHighlightCatchup: Boolean = false, val notificationHighlightCatchup: Boolean = false,

View File

@ -51,7 +51,8 @@ class HomeDrawerFragment : VectorBaseFragment() {
val groupListFragment = GroupListFragment.newInstance() val groupListFragment = GroupListFragment.newInstance()
replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer) replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer)
} }
session.liveUser(session.myUserId).observeK(this) { user -> session.liveUser(session.myUserId).observeK(this) { optionalUser ->
val user = optionalUser?.getOrNull()
if (user != null) { if (user != null) {
avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView) avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView)
homeDrawerUsernameView.text = user.displayName homeDrawerUsernameView.text = user.displayName

View File

@ -36,8 +36,7 @@ class HomeNavigator @Inject constructor() {
activity?.let { activity?.let {
it.drawerLayout?.closeDrawer(GravityCompat.START) it.drawerLayout?.closeDrawer(GravityCompat.START)
val args = HomeDetailParams(groupSummary.groupId, groupSummary.displayName, groupSummary.avatarUrl) val homeDetailFragment = HomeDetailFragment.newInstance()
val homeDetailFragment = HomeDetailFragment.newInstance(args)
it.replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer) it.replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer)
} }
} }

View File

@ -33,11 +33,13 @@ import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
const val ALL_COMMUNITIES_GROUP_ID = "ALL_COMMUNITIES_GROUP_ID" const val ALL_COMMUNITIES_GROUP_ID = "ALL_COMMUNITIES_GROUP_ID"
class GroupListViewModel @AssistedInject constructor(@Assisted initialState: GroupListViewState, class GroupListViewModel @AssistedInject constructor(@Assisted initialState: GroupListViewState,
private val selectedGroupHolder: SelectedGroupStore, private val selectedGroupStore: SelectedGroupStore,
private val session: Session, private val session: Session,
private val stringProvider: StringProvider private val stringProvider: StringProvider
) : VectorViewModel<GroupListViewState>(initialState) { ) : VectorViewModel<GroupListViewState>(initialState) {
@ -69,9 +71,13 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
private fun observeSelectionState() { private fun observeSelectionState() {
selectSubscribe(GroupListViewState::selectedGroup) { selectSubscribe(GroupListViewState::selectedGroup) {
if (it != null) { if (it != null) {
_openGroupLiveData.postLiveEvent(it) val selectedGroup = selectedGroupStore.get()?.orNull()
// We only wan to open group if the updated selectedGroup is a different one.
if (selectedGroup?.groupId != it.groupId) {
_openGroupLiveData.postLiveEvent(it)
}
val optionGroup = Option.fromNullable(it) val optionGroup = Option.fromNullable(it)
selectedGroupHolder.post(optionGroup) selectedGroupStore.post(optionGroup)
} }
} }
} }
@ -91,22 +97,33 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
} }
private fun observeGroupSummaries() { private fun observeGroupSummaries() {
session Observable.combineLatest<GroupSummary, List<GroupSummary>, List<GroupSummary>>(
.rx() session
.liveGroupSummaries() .rx()
// Keep only joined groups. Group invitations will be managed later .liveUser(session.myUserId)
.map { it.filter { groupSummary -> groupSummary.membership == Membership.JOIN } } .map { optionalUser ->
.map { GroupSummary(
val myUser = session.getUser(session.myUserId) groupId = ALL_COMMUNITIES_GROUP_ID,
val allCommunityGroup = GroupSummary( membership = Membership.JOIN,
groupId = ALL_COMMUNITIES_GROUP_ID, displayName = stringProvider.getString(R.string.group_all_communities),
membership = Membership.JOIN, avatarUrl = optionalUser.getOrNull()?.avatarUrl ?: "")
displayName = stringProvider.getString(R.string.group_all_communities), },
avatarUrl = myUser?.avatarUrl ?: "") session
listOf(allCommunityGroup) + it .rx()
.liveGroupSummaries()
// Keep only joined groups. Group invitations will be managed later
.map { it.filter { groupSummary -> groupSummary.membership == Membership.JOIN } },
BiFunction { allCommunityGroup, communityGroups ->
listOf(allCommunityGroup) + communityGroups
} }
)
.execute { async -> .execute { async ->
val newSelectedGroup = selectedGroup ?: async()?.firstOrNull() val currentSelectedGroupId = selectedGroup?.groupId
val newSelectedGroup = if (currentSelectedGroupId != null) {
async()?.find { it.groupId == currentSelectedGroupId }
} else {
async()?.firstOrNull()
}
copy(asyncGroups = async, selectedGroup = newSelectedGroup) copy(asyncGroups = async, selectedGroup = newSelectedGroup)
} }
} }