Merge pull request #593 from vector-im/feature/group_avatar

Group avatar live
This commit is contained in:
ganfra 2019-10-01 11:45:43 +02:00 committed by GitHub
commit aea34da81e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 177 additions and 85 deletions

View File

@ -13,6 +13,7 @@ Other changes:
Bugfix: Bugfix:
- Fix issue on upload error in loop (#587) - Fix issue on upload error in loop (#587)
- 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

@ -0,0 +1,42 @@
/*
* 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.util
data class Optional<T : Any> constructor(private val value: T?) {
fun get(): T {
return value!!
}
fun getOrNull(): T? {
return value
}
fun getOrElse(fn: () -> T): T {
return value ?: fn()
}
companion object {
fun <T : Any> from(value: T?): Optional<T> {
return Optional(value)
}
}
}
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()
fun clear() {
storeRelay = createRelay()
}
fun get(): T? {
return storeRelay.value
}
fun observe(): Observable<T> {
return storeRelay.hide().observeOn(Schedulers.computation())
}
fun post(value: T) {
storeRelay.accept(value)
}
private fun createRelay(): BehaviorRelay<T> {
return if (defaultValue == null) {
BehaviorRelay.create<T>() BehaviorRelay.create<T>()
} else { } else {
BehaviorRelay.createDefault(defaultValue) BehaviorRelay.createDefault(defaultValue)
} }
fun observe(): Observable<T> {
return storeSubject.hide().observeOn(Schedulers.computation())
}
fun post(value: T) {
storeSubject.accept(value)
} }
} }

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,18 +93,27 @@ 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 }
?: 0
val peopleHasHighlight = summaries val peopleHasHighlight = summaries
.filter { it.isDirect } .filter { it.isDirect }
.any { it.highlightCount > 0 } .any { it.highlightCount > 0 }
@ -107,9 +121,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
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 }
?: 0
val roomsHasHighlight = summaries val roomsHasHighlight = summaries
.filter { !it.isDirect } .filter { !it.isDirect }
.any { it.highlightCount > 0 } .any { it.highlightCount > 0 }
@ -125,7 +137,6 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
) )
} }
} }
}
.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) {
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) _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() {
Observable.combineLatest<GroupSummary, List<GroupSummary>, List<GroupSummary>>(
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 session
.rx() .rx()
.liveGroupSummaries() .liveGroupSummaries()
// Keep only joined groups. Group invitations will be managed later // Keep only joined groups. Group invitations will be managed later
.map { it.filter { groupSummary -> groupSummary.membership == Membership.JOIN } } .map { it.filter { groupSummary -> groupSummary.membership == Membership.JOIN } },
.map { BiFunction { allCommunityGroup, communityGroups ->
val myUser = session.getUser(session.myUserId) listOf(allCommunityGroup) + communityGroups
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
} }
)
.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)
} }
} }