diff --git a/CHANGES.md b/CHANGES.md index 7c37f66b4d..531bc25e25 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ Bugfix: - Fix issue on upload error in loop (#587) - 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) + - after login, the icon in the top left is a green 'A' for (all communities) rather than my avatar (#267) Translations: - diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt index a1943bbe1c..9eaeff762e 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt @@ -20,7 +20,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import io.reactivex.Observable import io.reactivex.android.MainThreadDisposable -import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers private class LiveDataObservable( diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index f3fb06a45a..0d0cb2f461 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -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.sync.SyncState 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.Single @@ -45,6 +46,10 @@ class RxSession(private val session: Session) { return session.livePushers().asObservable() } + fun liveUser(userId: String): Observable> { + return session.liveUser(userId).asObservable().distinctUntilChanged() + } + fun liveUsers(): Observable> { return session.liveUsers().asObservable() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index d3c58edd94..97b02fafea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -21,6 +21,7 @@ import androidx.paging.PagedList import im.vector.matrix.android.api.MatrixCallback 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.Optional /** * 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 * @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 + fun liveUser(userId: String): LiveData> /** * Observe a live list of users sorted alphabetically diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt index abe2d23993..19fbe2cc88 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt @@ -37,4 +37,6 @@ data class Optional constructor(private val value: T?) { } } -} \ No newline at end of file +} + +fun T?.toOptional() = Optional(this) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index 2925997347..40e79d7735 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -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.model.User 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.mapper.asDomain 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() } - override fun liveUser(userId: String): LiveData { + override fun liveUser(userId: String): LiveData> { val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> UserEntity.where(realm, userId) } @@ -74,6 +76,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona results .map { it.asDomain() } .firstOrNull() + .toOptional() } } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt b/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt index 89780b2463..94f2a9c912 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt @@ -20,19 +20,31 @@ import com.jakewharton.rxrelay2.BehaviorRelay import io.reactivex.Observable import io.reactivex.schedulers.Schedulers -open class RxStore(defaultValue: T? = null) { +open class RxStore(private val defaultValue: T? = null) { - private val storeSubject: BehaviorRelay = if (defaultValue == null) { - BehaviorRelay.create() - } else { - BehaviorRelay.createDefault(defaultValue) + var storeRelay = createRelay() + + fun clear() { + storeRelay = createRelay() + } + + fun get(): T? { + return storeRelay.value } fun observe(): Observable { - return storeSubject.hide().observeOn(Schedulers.computation()) + return storeRelay.hide().observeOn(Schedulers.computation()) } fun post(value: T) { - storeSubject.accept(value) + storeRelay.accept(value) + } + + private fun createRelay(): BehaviorRelay { + return if (defaultValue == null) { + BehaviorRelay.create() + } else { + BehaviorRelay.createDefault(defaultValue) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt index f8c1eca19e..971f3d602c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt @@ -98,7 +98,8 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState: override fun onCleared() { super.onCleared() - + selectedGroupStore.clear() + homeRoomListStore.clear() session.removeListener(this) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index 112fae1b24..955973011c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -17,18 +17,17 @@ package im.vector.riotx.features.home import android.os.Bundle -import android.os.Parcelable import android.view.LayoutInflater import androidx.core.view.forEachIndexed import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders -import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationMenuView 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.group.model.GroupSummary import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent 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.UnreadCounterBadgeView import im.vector.riotx.features.workers.signout.SignOutViewModel -import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_home_detail.* +import timber.log.Timber 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_PEOPLE = 1 private const val INDEX_ROOMS = 2 class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { - private val params: HomeDetailParams by args() private val unreadCounterBadgeViews = arrayListOf() private val viewModel: HomeDetailViewModel by fragmentViewModel() @@ -84,11 +74,25 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { setupToolbar() setupKeysBackupBanner() + viewModel.selectSubscribe(this, HomeDetailViewState::groupSummary) { groupSummary -> + onGroupChange(groupSummary.orNull()) + } viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode -> switchDisplayMode(displayMode) } } + private fun onGroupChange(groupSummary: GroupSummary?) { + groupSummary?.let { + avatarRenderer.render( + it.avatarUrl, + it.groupId, + it.displayName, + groupToolbarAvatarImageView + ) + } + } + private fun setupKeysBackupBanner() { // Keys backup banner // 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) } groupToolbar.title = "" - avatarRenderer.render( - params.groupAvatar, - params.groupId, - params.groupName, - groupToolbarAvatarImageView - ) groupToolbarAvatarImageView.setOnClickListener { navigationViewModel.goTo(HomeActivity.Navigation.OpenDrawer) } @@ -199,6 +197,7 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { } override fun invalidate() = withState(viewModel) { + Timber.v(it.toString()) unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup)) unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople)) unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms)) @@ -207,10 +206,8 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { companion object { - fun newInstance(args: HomeDetailParams): HomeDetailFragment { - return HomeDetailFragment().apply { - setArguments(args) - } + fun newInstance(): HomeDetailFragment { + return HomeDetailFragment() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index 688d2b6b7b..c8e3231362 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -25,6 +25,8 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.rx.rx import im.vector.riotx.core.di.HasScreenInjector 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.ui.UiStateRepository import io.reactivex.schedulers.Schedulers @@ -36,7 +38,9 @@ import io.reactivex.schedulers.Schedulers class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState, private val session: Session, private val uiStateRepository: UiStateRepository, - private val homeRoomListStore: HomeRoomListObservableStore) + private val selectedGroupStore: SelectedGroupStore, + private val homeRoomListStore: HomeRoomListObservableStore, + private val stringProvider: StringProvider) : VectorViewModel(initialState) { @AssistedInject.Factory @@ -62,6 +66,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho init { observeSyncState() + observeSelectedGroupStore() observeRoomSummaries() } @@ -88,42 +93,48 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho .disposeOnClear() } + private fun observeSelectedGroupStore() { + selectedGroupStore + .observe() + .subscribe { + setState { + copy(groupSummary = it) + } + } + .disposeOnClear() + } + private fun observeRoomSummaries() { homeRoomListStore .observe() .observeOn(Schedulers.computation()) - .subscribe { list -> - list.let { summaries -> - val peopleNotifications = summaries - .filter { it.isDirect } - .map { it.notificationCount } - .takeIf { it.isNotEmpty() } - ?.sumBy { i -> i } - ?: 0 - val peopleHasHighlight = summaries - .filter { it.isDirect } - .any { it.highlightCount > 0 } + .map { it.asSequence() } + .subscribe { summaries -> + val peopleNotifications = summaries + .filter { it.isDirect } + .map { it.notificationCount } + .sumBy { i -> i } + val peopleHasHighlight = summaries + .filter { it.isDirect } + .any { it.highlightCount > 0 } - val roomsNotifications = summaries - .filter { !it.isDirect } - .map { it.notificationCount } - .takeIf { it.isNotEmpty() } - ?.sumBy { i -> i } - ?: 0 - val roomsHasHighlight = summaries - .filter { !it.isDirect } - .any { it.highlightCount > 0 } + val roomsNotifications = summaries + .filter { !it.isDirect } + .map { it.notificationCount } + .sumBy { i -> i } + val roomsHasHighlight = summaries + .filter { !it.isDirect } + .any { it.highlightCount > 0 } - setState { - copy( - notificationCountCatchup = peopleNotifications + roomsNotifications, - notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight, - notificationCountPeople = peopleNotifications, - notificationHighlightPeople = peopleHasHighlight, - notificationCountRooms = roomsNotifications, - notificationHighlightRooms = roomsHasHighlight - ) - } + setState { + copy( + notificationCountCatchup = peopleNotifications + roomsNotifications, + notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight, + notificationCountPeople = peopleNotifications, + notificationHighlightPeople = peopleHasHighlight, + notificationCountRooms = roomsNotifications, + notificationHighlightRooms = roomsHasHighlight + ) } } .disposeOnClear() diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt index a8f89cc566..cb2c07835d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt @@ -16,11 +16,14 @@ package im.vector.riotx.features.home +import arrow.core.Option 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.riotx.features.home.room.list.RoomListFragment data class HomeDetailViewState( + val groupSummary: Option = Option.empty(), val displayMode: RoomListFragment.DisplayMode = RoomListFragment.DisplayMode.HOME, val notificationCountCatchup: Int = 0, val notificationHighlightCatchup: Boolean = false, diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt index ad39839321..e5f0c5b2d3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt @@ -51,7 +51,8 @@ class HomeDrawerFragment : VectorBaseFragment() { val groupListFragment = GroupListFragment.newInstance() 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) { avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView) homeDrawerUsernameView.text = user.displayName diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeNavigator.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeNavigator.kt index 680d7bbc33..ca1d12d95f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeNavigator.kt @@ -36,8 +36,7 @@ class HomeNavigator @Inject constructor() { activity?.let { it.drawerLayout?.closeDrawer(GravityCompat.START) - val args = HomeDetailParams(groupSummary.groupId, groupSummary.displayName, groupSummary.avatarUrl) - val homeDetailFragment = HomeDetailFragment.newInstance(args) + val homeDetailFragment = HomeDetailFragment.newInstance() it.replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt index 0be22e411e..c7278ff81d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt @@ -33,11 +33,13 @@ import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.StringProvider 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" class GroupListViewModel @AssistedInject constructor(@Assisted initialState: GroupListViewState, - private val selectedGroupHolder: SelectedGroupStore, + private val selectedGroupStore: SelectedGroupStore, private val session: Session, private val stringProvider: StringProvider ) : VectorViewModel(initialState) { @@ -69,9 +71,13 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro private fun observeSelectionState() { selectSubscribe(GroupListViewState::selectedGroup) { 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) - selectedGroupHolder.post(optionGroup) + selectedGroupStore.post(optionGroup) } } } @@ -91,22 +97,33 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro } private fun observeGroupSummaries() { - session - .rx() - .liveGroupSummaries() - // Keep only joined groups. Group invitations will be managed later - .map { it.filter { groupSummary -> groupSummary.membership == Membership.JOIN } } - .map { - val myUser = session.getUser(session.myUserId) - val allCommunityGroup = GroupSummary( - groupId = ALL_COMMUNITIES_GROUP_ID, - membership = Membership.JOIN, - displayName = stringProvider.getString(R.string.group_all_communities), - avatarUrl = myUser?.avatarUrl ?: "") - listOf(allCommunityGroup) + it + Observable.combineLatest, List>( + session + .rx() + .liveUser(session.myUserId) + .map { optionalUser -> + GroupSummary( + groupId = ALL_COMMUNITIES_GROUP_ID, + membership = Membership.JOIN, + displayName = stringProvider.getString(R.string.group_all_communities), + avatarUrl = optionalUser.getOrNull()?.avatarUrl ?: "") + }, + session + .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 -> - 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) } }