mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 20:06:51 +03:00
Merge pull request #593 from vector-im/feature/group_avatar
Group avatar live
This commit is contained in:
commit
aea34da81e
14 changed files with 177 additions and 85 deletions
|
@ -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:
|
||||
-
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,7 +98,8 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
|
|||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
selectedGroupStore.clear()
|
||||
homeRoomListStore.clear()
|
||||
session.removeListener(this)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue