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:
- 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:
-

View file

@ -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<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.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<Optional<User>> {
return session.liveUser(userId).asObservable().distinctUntilChanged()
}
fun liveUsers(): Observable<List<User>> {
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.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<User?>
fun liveUser(userId: String): LiveData<Optional<User>>
/**
* 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.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<User?> {
override fun liveUser(userId: String): LiveData<Optional<User>> {
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()
}
}

View file

@ -20,19 +20,31 @@ import com.jakewharton.rxrelay2.BehaviorRelay
import io.reactivex.Observable
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) {
BehaviorRelay.create<T>()
} else {
BehaviorRelay.createDefault(defaultValue)
var storeRelay = createRelay()
fun clear() {
storeRelay = createRelay()
}
fun get(): T? {
return storeRelay.value
}
fun observe(): Observable<T> {
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<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() {
super.onCleared()
selectedGroupStore.clear()
homeRoomListStore.clear()
session.removeListener(this)
}

View file

@ -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<UnreadCounterBadgeView>()
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()
}
}

View file

@ -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<HomeDetailViewState>(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()

View file

@ -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<GroupSummary> = Option.empty(),
val displayMode: RoomListFragment.DisplayMode = RoomListFragment.DisplayMode.HOME,
val notificationCountCatchup: Int = 0,
val notificationHighlightCatchup: Boolean = false,

View file

@ -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

View file

@ -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)
}
}

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.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<GroupListViewState>(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<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
.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)
}
}