Home: continue architecture rework. WIP

This commit is contained in:
ganfra 2019-05-16 19:14:02 +02:00 committed by Benoit Marty
parent 268730e71b
commit 275521db70
30 changed files with 416 additions and 346 deletions

View file

@ -18,6 +18,7 @@
package im.vector.matrix.android.api.session.user
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.user.model.User
/**
@ -32,4 +33,11 @@ interface UserService {
*/
fun getUser(userId: String): User?
/**
* Observe a live user from a userId
* @param userId the userId to look for.
* @return a Livedata of user with userId
*/
fun observeUser(userId: String): LiveData<User?>
}

View file

@ -14,23 +14,22 @@
* limitations under the License.
*/
package im.vector.riotredesign.features.home.room.list
package im.vector.matrix.android.internal.database.mapper
import android.content.SharedPreferences
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.database.model.UserEntity
private const val SHARED_PREFS_SELECTED_ROOM_KEY = "SHARED_PREFS_SELECTED_ROOM_KEY"
internal object UserMapper {
class RoomSelectionRepository(private val sharedPreferences: SharedPreferences) {
fun lastSelectedRoom(): String? {
return sharedPreferences.getString(SHARED_PREFS_SELECTED_ROOM_KEY, null)
fun map(userEntity: UserEntity): User {
return User(
userEntity.userId,
userEntity.displayName,
userEntity.avatarUrl
)
}
}
fun saveLastSelectedRoom(roomId: String) {
sharedPreferences.edit()
.putString(SHARED_PREFS_SELECTED_ROOM_KEY, roomId)
.apply()
internal fun UserEntity.asDomain(): User {
return UserMapper.map(this)
}
}

View file

@ -231,6 +231,11 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
return userService.getUser(userId)
}
override fun observeUser(userId: String): LiveData<User?> {
assert(isOpen)
return userService.observeUser(userId)
}
// Private methods *****************************************************************************
private fun assertMainThread() {

View file

@ -18,9 +18,13 @@
package im.vector.matrix.android.internal.session.user
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.user.model.User
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
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.fetchCopied
@ -31,10 +35,17 @@ internal class DefaultUserService(private val monarchy: Monarchy) : UserService
val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() }
?: return null
return User(
userEntity.userId,
userEntity.displayName,
userEntity.avatarUrl
)
return userEntity.asDomain()
}
override fun observeUser(userId: String): LiveData<User?> {
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
UserEntity.where(realm, userId)
}
return Transformations.map(liveRealmData) { results ->
results
.map { it.asDomain() }
.firstOrNull()
}
}
}

View file

@ -24,8 +24,6 @@ import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringArrayProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
import org.koin.dsl.module.module
@ -50,18 +48,10 @@ class AppModule(private val context: Context) {
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
}
single {
RoomSelectionRepository(get())
}
single {
SelectedGroupStore()
}
single {
VisibleRoomStore()
}
single {
RoomSummaryComparator()
}
@ -78,6 +68,5 @@ class AppModule(private val context: Context) {
Matrix.getInstance().currentSession!!
}
}
}

View file

@ -109,8 +109,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
override fun configure(toolbar: Toolbar) {
setSupportActionBar(toolbar)
supportActionBar?.setHomeButtonEnabled(true)
supportActionBar?.setDisplayUseLogoEnabled(true)
}
override fun getMenuRes() = R.menu.home
@ -121,10 +119,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
drawerLayout.openDrawer(GravityCompat.START)
return true
}
R.id.sliding_menu_settings -> {
startActivity(VectorSettingsActivity.getIntent(this, "TODO"))
return true
}
R.id.sliding_menu_sign_out -> {
SignOutUiWorker(this).perform(Matrix.getInstance().currentSession!!)
return true

View file

@ -18,26 +18,31 @@ package im.vector.riotredesign.features.home
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary
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.FilterService
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
import io.reactivex.rxkotlin.subscribeBy
import im.vector.riotredesign.features.home.group.ALL_COMMUNITIES_GROUP_ID
import im.vector.riotredesign.features.home.group.SelectedGroupStore
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit
data class EmptyState(val isEmpty: Boolean = true) : MvRxState
class HomeActivityViewModel(state: EmptyState,
private val session: Session,
roomSelectionRepository: RoomSelectionRepository
private val selectedGroupStore: SelectedGroupStore,
private val homeRoomListStore: HomeRoomListObservableStore
) : VectorViewModel<EmptyState>(state), Session.Listener {
companion object : MvRxViewModelFactory<HomeActivityViewModel, EmptyState> {
@ -45,8 +50,9 @@ class HomeActivityViewModel(state: EmptyState,
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? {
val session = Matrix.getInstance().currentSession!!
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
return HomeActivityViewModel(state, session, roomSelectionRepository)
val selectedGroupStore = viewModelContext.activity.get<SelectedGroupStore>()
val homeRoomListObservableSource = viewModelContext.activity.get<HomeRoomListObservableStore>()
return HomeActivityViewModel(state, session, selectedGroupStore, homeRoomListObservableSource)
}
}
@ -54,29 +60,41 @@ class HomeActivityViewModel(state: EmptyState,
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
val openRoomLiveData: LiveData<LiveEvent<String>>
get() = _openRoomLiveData
init {
session.addListener(this)
val lastSelectedRoomId = roomSelectionRepository.lastSelectedRoom()
if (lastSelectedRoomId == null || session.getRoom(lastSelectedRoomId) == null) {
getTheFirstRoomWhenAvailable()
observeRoomAndGroup()
}
private fun observeRoomAndGroup() {
Observable
.combineLatest<List<RoomSummary>, Option<GroupSummary>, List<RoomSummary>>(
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
selectedGroupStore.observe(),
BiFunction { rooms, selectedGroupOption ->
val selectedGroup = selectedGroupOption.orNull()
val filteredDirectRooms = rooms
.filter { it.isDirect }
.filter {
if (selectedGroup == null || selectedGroup.groupId == ALL_COMMUNITIES_GROUP_ID) {
true
} else {
_openRoomLiveData.postValue(LiveEvent(lastSelectedRoomId))
it.otherMemberIds
.intersect(selectedGroup.userIds)
.isNotEmpty()
}
}
private fun getTheFirstRoomWhenAvailable() {
session.rx().liveRoomSummaries()
.filter { it.isNotEmpty() }
.first(emptyList())
.subscribeBy {
val firstRoom = it.firstOrNull()
if (firstRoom != null) {
_openRoomLiveData.postValue(LiveEvent(firstRoom.roomId))
val filteredGroupRooms = rooms
.filter { !it.isDirect }
.filter {
selectedGroup?.groupId == ALL_COMMUNITIES_GROUP_ID
|| selectedGroup?.roomIds?.contains(it.roomId) ?: true
}
filteredDirectRooms + filteredGroupRooms
}
)
.subscribe {
homeRoomListStore.post(it)
}
.disposeOnClear()
}
@ -87,8 +105,6 @@ class HomeActivityViewModel(state: EmptyState,
session.createRoom(createRoomParams, object : MatrixCallback<String> {
override fun onSuccess(data: String) {
_isLoading.value = false
// Open room id
_openRoomLiveData.postValue(LiveEvent(data))
}
override fun onFailure(failure: Throwable) {

View file

@ -19,9 +19,11 @@ package im.vector.riotredesign.features.home
import android.os.Bundle
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.observeK
import im.vector.riotredesign.core.extensions.replaceChildFragment
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.home.group.GroupListFragment
import im.vector.riotredesign.features.settings.VectorSettingsActivity
import kotlinx.android.synthetic.main.fragment_home_drawer.*
class HomeDrawerFragment : VectorBaseFragment() {
@ -42,12 +44,15 @@ class HomeDrawerFragment : VectorBaseFragment() {
replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer)
}
val session = Matrix.getInstance().currentSession ?: return
val user = session.getUser(session.sessionParams.credentials.userId)
session.observeUser(session.sessionParams.credentials.userId).observeK(this) { user ->
if (user != null) {
AvatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView)
homeDrawerUsernameView.text = user.displayName
homeDrawerUserIdView.text = user.userId
}
}
homeDrawerHeaderSettingsView.setOnClickListener {
startActivity(VectorSettingsActivity.getIntent(requireContext(), "TODO"))
}
}
}

View file

@ -25,7 +25,14 @@ import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserControl
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotredesign.features.home.group.GroupSummaryController
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.factory.*
import im.vector.riotredesign.features.home.room.detail.timeline.factory.CallItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.factory.DefaultItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.factory.MessageItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomHistoryVisibilityItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomMemberItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomNameItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomTopicItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.list.RoomSummaryController
@ -49,6 +56,10 @@ class HomeModule {
HomeNavigator()
}
scope(HOME_SCOPE) {
HomeRoomListObservableStore()
}
scope(HOME_SCOPE) {
HomePermalinkHandler(get())
}

View file

@ -0,0 +1,22 @@
/*
* 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.riotredesign.features.home
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.core.utils.RxStore
class HomeRoomListObservableStore : RxStore<List<RoomSummary>>(emptyList())

View file

@ -54,15 +54,18 @@ class GroupListViewModel(initialState: GroupListViewState,
init {
observeGroupSummaries()
observeState()
observeSelectionState()
}
private fun observeState() {
subscribe {
val optionGroup = Option.fromNullable(it.selectedGroup)
private fun observeSelectionState() {
selectSubscribe(GroupListViewState::selectedGroup) {
if (it != null) {
_openGroupLiveData.postValue(LiveEvent(it))
val optionGroup = Option.fromNullable(it)
selectedGroupHolder.post(optionGroup)
}
}
}
fun accept(action: GroupListActions) {
when (action) {
@ -75,7 +78,6 @@ class GroupListViewModel(initialState: GroupListViewState,
private fun handleSelectGroup(action: GroupListActions.SelectGroup) = withState { state ->
if (state.selectedGroup?.groupId != action.groupSummary.groupId) {
setState { copy(selectedGroup = action.groupSummary) }
_openGroupLiveData.postValue(LiveEvent(action.groupSummary))
}
}
@ -91,7 +93,8 @@ class GroupListViewModel(initialState: GroupListViewState,
listOf(allCommunityGroup) + it
}
.execute { async ->
copy(asyncGroups = async)
val newSelectedGroup = selectedGroup ?: async()?.firstOrNull()
copy(asyncGroups = async, selectedGroup = newSelectedGroup)
}
}

View file

@ -16,7 +16,6 @@
package im.vector.riotredesign.features.home.group
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
@ -24,6 +23,7 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.platform.CheckableFrameLayout
import im.vector.riotredesign.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_group)
@ -39,13 +39,14 @@ abstract class GroupSummaryItem : VectorEpoxyModel<GroupSummaryItem.Holder>() {
super.bind(holder)
holder.rootView.setOnClickListener { listener?.invoke() }
holder.groupNameView.text = groupName
holder.rootView.isChecked = selected
AvatarRenderer.render(avatarUrl, groupId, groupName.toString(), holder.avatarImageView)
}
class Holder : VectorEpoxyHolder() {
val avatarImageView by bind<ImageView>(R.id.groupAvatarImageView)
val groupNameView by bind<TextView>(R.id.groupNameView)
val rootView by bind<ViewGroup>(R.id.itemGroupLayout)
val rootView by bind<CheckableFrameLayout>(R.id.itemGroupLayout)
}
}

View file

@ -18,15 +18,10 @@
package im.vector.riotredesign.features.home.group
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Parcelable
import com.airbnb.mvrx.args
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.replaceChildFragment
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.home.AvatarRenderer
@ -42,9 +37,12 @@ data class SelectedGroupParams(
val groupAvatar: String
) : Parcelable
private const val CURRENT_DISPLAY_MODE = "CURRENT_DISPLAY_MODE"
class SelectedGroupFragment : VectorBaseFragment() {
private val selectedGroupParams: SelectedGroupParams by args()
private lateinit var currentDisplayMode: RoomListFragment.DisplayMode
override fun getLayoutResId(): Int {
return R.layout.fragment_selected_group
@ -53,31 +51,36 @@ class SelectedGroupFragment : VectorBaseFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (savedInstanceState == null) {
updateSelectedFragment(RoomListFragment.DisplayMode.HOME)
toolbar.setTitle(RoomListFragment.DisplayMode.HOME.titleRes)
currentDisplayMode = RoomListFragment.DisplayMode.HOME
} else {
currentDisplayMode = savedInstanceState.getSerializable(CURRENT_DISPLAY_MODE) as? RoomListFragment.DisplayMode
?: RoomListFragment.DisplayMode.HOME
}
renderState(currentDisplayMode)
setupBottomNavigationView()
setupToolbar()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putSerializable(CURRENT_DISPLAY_MODE, currentDisplayMode)
super.onSaveInstanceState(outState)
}
private fun setupToolbar() {
val parentActivity = vectorBaseActivity
if (parentActivity is ToolbarConfigurable) {
parentActivity.configure(toolbar)
}
val toolbarLogoTarget = object : SimpleTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
toolbar.logo = resource
}
parentActivity.configure(groupToolbar)
}
groupToolbar.title = ""
AvatarRenderer.render(
requireContext(),
GlideApp.with(this),
selectedGroupParams.groupAvatar,
selectedGroupParams.groupId,
selectedGroupParams.groupName,
toolbarLogoTarget
groupToolbarAvatarImageView
)
groupToolbarAvatarImageView.setOnClickListener {
}
}
private fun setupBottomNavigationView() {
@ -87,16 +90,29 @@ class SelectedGroupFragment : VectorBaseFragment() {
it.itemId == R.id.bottom_action_rooms -> RoomListFragment.DisplayMode.ROOMS
else -> RoomListFragment.DisplayMode.HOME
}
updateSelectedFragment(displayMode)
toolbar.setTitle(displayMode.titleRes)
if (currentDisplayMode != displayMode) {
currentDisplayMode = displayMode
renderState(displayMode)
}
true
}
}
private fun renderState(displayMode: RoomListFragment.DisplayMode) {
groupToolbarTitleView.setText(displayMode.titleRes)
updateSelectedFragment(displayMode)
}
private fun updateSelectedFragment(displayMode: RoomListFragment.DisplayMode) {
val roomListParams = RoomListParams(displayMode)
val roomListFragment = RoomListFragment.newInstance(roomListParams)
replaceChildFragment(roomListFragment, R.id.roomListContainer)
val fragmentTag = "FRAGMENT_TAG_${displayMode.name}"
var fragment = childFragmentManager.findFragmentByTag(fragmentTag)
if (fragment == null) {
fragment = RoomListFragment.newInstance(RoomListParams(displayMode))
}
childFragmentManager.beginTransaction()
.replace(R.id.roomListContainer, fragment, fragmentTag)
.addToBackStack(fragmentTag)
.commit()
}
companion object {

View file

@ -25,7 +25,6 @@ sealed class RoomDetailActions {
data class SendMessage(val text: String) : RoomDetailActions()
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
object IsDisplayed : RoomDetailActions()
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()

View file

@ -21,11 +21,13 @@ package im.vector.riotredesign.features.home.room.detail
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseActivity
class RoomDetailActivity : VectorBaseActivity() {
class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
override fun getLayoutRes(): Int {
return R.layout.activity_room_detail
@ -41,6 +43,14 @@ class RoomDetailActivity : VectorBaseActivity() {
}
}
override fun configure(toolbar: Toolbar) {
setSupportActionBar(toolbar)
supportActionBar?.let {
it.setDisplayShowHomeEnabled(true)
it.setDisplayHomeAsUpEnabled(true)
}
}
companion object {
private const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS"

View file

@ -64,6 +64,7 @@ import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.extensions.hideKeyboard
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.utils.*
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
@ -168,6 +169,7 @@ class RoomDetailFragment :
super.onActivityCreated(savedInstanceState)
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
setupToolbar()
setupRecyclerView()
setupComposer()
setupAttachmentButton()
@ -187,6 +189,13 @@ class RoomDetailFragment :
})
}
private fun setupToolbar() {
val parentActivity = vectorBaseActivity
if (parentActivity is ToolbarConfigurable) {
parentActivity.configure(roomToolbar)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK && data != null) {
@ -204,11 +213,6 @@ class RoomDetailFragment :
}
}
override fun onResume() {
super.onResume()
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
}
// PRIVATE METHODS *****************************************************************************
private fun setupRecyclerView() {
@ -399,13 +403,13 @@ class RoomDetailFragment :
private fun renderRoomSummary(state: RoomDetailViewState) {
state.asyncRoomSummary()?.let {
toolbarTitleView.text = it.displayName
AvatarRenderer.render(it, toolbarAvatarImageView)
roomToolbarTitleView.text = it.displayName
AvatarRenderer.render(it, roomToolbarAvatarImageView)
if (it.topic.isNotEmpty()) {
toolbarSubtitleView.visibility = View.VISIBLE
toolbarSubtitleView.text = it.topic
roomToolbarSubtitleView.visibility = View.VISIBLE
roomToolbarSubtitleView.text = it.topic
} else {
toolbarSubtitleView.visibility = View.GONE
roomToolbarSubtitleView.visibility = View.GONE
}
}
}

View file

@ -33,7 +33,6 @@ import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.command.CommandParser
import im.vector.riotredesign.features.command.ParsedCommand
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get
@ -42,8 +41,7 @@ import java.util.*
import java.util.concurrent.TimeUnit
class RoomDetailViewModel(initialState: RoomDetailViewState,
private val session: Session,
private val visibleRoomHolder: VisibleRoomStore
private val session: Session
) : VectorViewModel<RoomDetailViewState>(initialState) {
private val room = session.getRoom(initialState.roomId)!!
@ -59,8 +57,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? {
val currentSession = viewModelContext.activity.get<Session>()
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>()
return RoomDetailViewModel(state, currentSession, visibleRoomHolder)
return RoomDetailViewModel(state, currentSession)
}
}
@ -76,7 +73,6 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
fun process(action: RoomDetailActions) {
when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
is RoomDetailActions.SendMedia -> handleSendMedia(action)
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
is RoomDetailActions.LoadMore -> handleLoadMore(action)
@ -255,10 +251,6 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
displayedEventsObservable.accept(action)
}
private fun handleIsDisplayed() {
visibleRoomHolder.post(roomId)
}
private fun handleLoadMore(action: RoomDetailActions.LoadMore) {
timeline.paginate(action.direction, PAGINATION_COUNT)
}

View file

@ -23,6 +23,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.room.model.RoomSummary
@ -61,6 +62,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback {
}
}
private val roomListParams: RoomListParams by args()
private val roomController by inject<RoomSummaryController>()
private val homeNavigator by inject<HomeNavigator>()
private val roomListViewModel: RoomListViewModel by fragmentViewModel()
@ -71,12 +73,20 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback {
super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(HomeModule.ROOM_LIST_SCOPE))
setupRecyclerView()
setupCreateRoomButton()
roomListViewModel.subscribe { renderState(it) }
roomListViewModel.openRoomLiveData.observeEvent(this) {
homeNavigator.openRoomDetail(it, null)
}
}
private fun setupCreateRoomButton() {
createRoomButton.setImageResource(R.drawable.ic_add_white)
createRoomButton.setOnClickListener {
vectorBaseActivity.notImplemented()
}
}
private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()

View file

@ -23,28 +23,19 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.home.group.ALL_COMMUNITIES_GROUP_ID
import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import io.reactivex.Observable
import io.reactivex.functions.Function3
import im.vector.riotredesign.features.home.HomeRoomListObservableStore
import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit
typealias RoomListFilterName = CharSequence
class RoomListViewModel(initialState: RoomListViewState,
private val session: Session,
private val selectedGroupHolder: SelectedGroupStore,
private val visibleRoomHolder: VisibleRoomStore,
private val roomSelectionRepository: RoomSelectionRepository,
private val homeRoomListObservableSource: HomeRoomListObservableStore,
private val roomSummaryComparator: RoomSummaryComparator)
: VectorViewModel<RoomListViewState>(initialState) {
@ -53,11 +44,9 @@ class RoomListViewModel(initialState: RoomListViewState,
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? {
val currentSession = viewModelContext.activity.get<Session>()
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>()
val homeRoomListObservableSource = viewModelContext.activity.get<HomeRoomListObservableStore>()
val roomSummaryComparator = viewModelContext.activity.get<RoomSummaryComparator>()
return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository, roomSummaryComparator)
return RoomListViewModel(state, currentSession, homeRoomListObservableSource, roomSummaryComparator)
}
}
@ -70,7 +59,6 @@ class RoomListViewModel(initialState: RoomListViewState,
init {
observeRoomSummaries()
observeVisibleRoom()
}
fun accept(action: RoomListActions) {
@ -83,12 +71,9 @@ class RoomListViewModel(initialState: RoomListViewState,
// PRIVATE METHODS *****************************************************************************
private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state ->
if (state.visibleRoomId != action.roomSummary.roomId) {
roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId)
private fun handleSelectRoom(action: RoomListActions.SelectRoom) {
_openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId))
}
}
private fun handleFilterRooms(action: RoomListActions.FilterRooms) {
val optionalFilter = Option.fromNullable(action.roomName)
@ -99,44 +84,10 @@ class RoomListViewModel(initialState: RoomListViewState,
this.toggle(action.category)
}
private fun observeVisibleRoom() {
visibleRoomHolder.observe()
.doOnNext {
setState { copy(visibleRoomId = it) }
}
.subscribe()
.disposeOnClear()
}
private fun observeRoomSummaries() {
Observable.combineLatest<List<RoomSummary>, Option<GroupSummary>, Option<RoomListFilterName>, RoomSummaries>(
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
selectedGroupHolder.observe(),
roomListFilter.throttleLast(300, TimeUnit.MILLISECONDS),
Function3 { rooms, selectedGroupOption, filterRoomOption ->
val filteredRooms = filterRooms(rooms, filterRoomOption)
val selectedGroup = selectedGroupOption.orNull()
val filteredDirectRooms = filteredRooms
.filter { it.isDirect }
.filter {
if (selectedGroup == null || selectedGroup.groupId == ALL_COMMUNITIES_GROUP_ID) {
true
} else {
it.otherMemberIds
.intersect(selectedGroup.userIds)
.isNotEmpty()
}
}
val filteredGroupRooms = filteredRooms
.filter { !it.isDirect }
.filter {
selectedGroup?.groupId == ALL_COMMUNITIES_GROUP_ID
|| selectedGroup?.roomIds?.contains(it.roomId) ?: true
}
buildRoomSummaries(filteredDirectRooms + filteredGroupRooms)
}
)
homeRoomListObservableSource.observe()
.map { buildRoomSummaries(it) }
.execute { async ->
copy(
asyncRooms = async
@ -144,17 +95,6 @@ class RoomListViewModel(initialState: RoomListViewState,
}
}
private fun filterRooms(rooms: List<RoomSummary>, filterRoomOption: Option<RoomListFilterName>): List<RoomSummary> {
val filterRoom = filterRoomOption.orNull()
return rooms.filter {
if (filterRoom.isNullOrBlank()) {
true
} else {
it.displayName.contains(other = filterRoom, ignoreCase = true)
}
}
}
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
val invites = ArrayList<RoomSummary>()
val favourites = ArrayList<RoomSummary>()

View file

@ -25,7 +25,6 @@ import im.vector.riotredesign.R
data class RoomListViewState(
val asyncRooms: Async<RoomSummaries> = Uninitialized,
val visibleRoomId: String? = null,
val isInviteExpanded: Boolean = true,
val isFavouriteRoomsExpanded: Boolean = true,
val isDirectRoomsExpanded: Boolean = true,

View file

@ -37,7 +37,7 @@ class RoomSummaryController(private val stringProvider: StringProvider
callback?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(summaries, viewState.visibleRoomId)
buildRoomModels(summaries)
}
}
}
@ -71,18 +71,16 @@ class RoomSummaryController(private val stringProvider: StringProvider
}
}
private fun buildRoomModels(summaries: List<RoomSummary>, selectedRoomId: String?) {
private fun buildRoomModels(summaries: List<RoomSummary>) {
summaries.forEach { roomSummary ->
val unreadCount = roomSummary.notificationCount
val showHighlighted = roomSummary.highlightCount > 0
val isSelected = roomSummary.roomId == selectedRoomId
roomSummaryItem {
id(roomSummary.roomId)
roomId(roomSummary.roomId)
roomName(roomSummary.displayName)
avatarUrl(roomSummary.avatarUrl)
selected(isSelected)
showHighlighted(showHighlighted)
unreadCount(unreadCount)
listener { callback?.onRoomSelected(roomSummary) }

View file

@ -16,6 +16,7 @@
package im.vector.riotredesign.features.home.room.list
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
@ -23,7 +24,6 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.platform.CheckableFrameLayout
import im.vector.riotredesign.features.home.AvatarRenderer
@ -33,7 +33,6 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
@EpoxyAttribute lateinit var roomName: CharSequence
@EpoxyAttribute lateinit var roomId: String
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var unreadCount: Int = 0
@EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null
@ -42,7 +41,6 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
override fun bind(holder: Holder) {
super.bind(holder)
holder.unreadCounterBadgeView.render(unreadCount, showHighlighted)
holder.rootView.isChecked = selected
holder.rootView.setOnClickListener { listener?.invoke() }
holder.titleView.text = roomName
AvatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
@ -52,7 +50,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
val titleView by bind<TextView>(R.id.roomNameView)
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
val rootView by bind<CheckableFrameLayout>(R.id.itemRoomLayout)
val rootView by bind<ViewGroup>(R.id.itemRoomLayout)
}
}

View file

@ -3,7 +3,7 @@
<item android:state_checked="true">
<shape>
<solid android:color="@android:color/white" />
<solid android:color="#10000000" />
<corners android:radius="4dp" />
</shape>
</item>

View file

@ -2,9 +2,10 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/stateView"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/homeDrawerHeader"
@ -46,12 +47,16 @@
app:layout_constraintTop_toBottomOf="@+id/homeDrawerUsernameView"
tools:text="@tools:sample/full_names" />
<ImageButton
<ImageView
android:id="@+id/homeDrawerHeaderSettingsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="32dp"
android:layout_height="32dp"
android:padding="6dp"
android:scaleType="fitCenter"
android:background="?attr/selectableItemBackground"
android:src="@drawable/ic_settings"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/homeDrawerUserIdView" />
app:layout_constraintTop_toBottomOf="@+id/homeDrawerUsernameView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -6,7 +6,7 @@
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:id="@+id/roomToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
@ -23,7 +23,7 @@
android:layout_height="match_parent">
<ImageView
android:id="@+id/toolbarAvatarImageView"
android:id="@+id/roomToolbarAvatarImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
@ -34,7 +34,7 @@
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/toolbarTitleView"
android:id="@+id/roomToolbarTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
@ -43,15 +43,15 @@
android:maxLines="1"
android:textColor="?vctr_toolbar_primary_text_color"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@+id/toolbarSubtitleView"
app:layout_constraintBottom_toTopOf="@+id/roomToolbarSubtitleView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/toolbarAvatarImageView"
app:layout_constraintStart_toEndOf="@+id/roomToolbarAvatarImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/toolbarSubtitleView"
android:id="@+id/roomToolbarSubtitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
@ -63,8 +63,8 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/toolbarAvatarImageView"
app:layout_constraintTop_toBottomOf="@+id/toolbarTitleView"
app:layout_constraintStart_toEndOf="@+id/roomToolbarAvatarImageView"
app:layout_constraintTop_toBottomOf="@+id/roomToolbarTitleView"
tools:text="@tools:sample/date/day_of_week" />
@ -79,7 +79,7 @@
app:layout_constraintBottom_toTopOf="@+id/composerDivider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintTop_toBottomOf="@id/roomToolbar"
tools:listitem="@layout/item_timeline_event_base" />
<View
@ -148,7 +148,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layout_constraintTop_toBottomOf="@+id/roomToolbar"
app:layout_constraintVertical_bias="1.0"
tools:visibility="visible" />

View file

@ -12,4 +12,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/createRoomButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp" />
</im.vector.riotredesign.core.platform.StateView>

View file

@ -2,11 +2,12 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:id="@+id/groupToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
@ -14,14 +15,42 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/groupToolbarAvatarImageView"
android:layout_width="40dp"
android:layout_height="40dp"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/groupToolbarTitleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?vctr_toolbar_primary_text_color"
android:textSize="18sp"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
</androidx.appcompat.widget.Toolbar>
<FrameLayout
android:id="@+id/roomListContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
app:layout_constraintTop_toBottomOf="@+id/groupToolbar" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView"

View file

@ -1,15 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<im.vector.riotredesign.core.platform.CheckableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemGroupLayout"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@android:color/transparent"
android:layout_height="wrap_content"
android:background="@drawable/bg_group_item"
android:clickable="true"
android:focusable="true"
android:padding="16dp">
android:foreground="?attr/selectableItemBackground"
android:paddingStart="8dp"
android:paddingLeft="8dp"
android:paddingEnd="16dp"
android:paddingRight="16dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:duplicateParentState="true">
<ImageView
android:id="@+id/groupAvatarImageView"
@ -46,3 +56,5 @@
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</im.vector.riotredesign.core.platform.CheckableFrameLayout>

View file

@ -1,26 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<im.vector.riotredesign.core.platform.CheckableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemRoomLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_room_item"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="48dp"
android:paddingStart="8dp"
android:paddingLeft="8dp"
android:paddingEnd="16dp"
android:paddingRight="16dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:duplicateParentState="true"
android:minHeight="48dp">
<ImageView
android:id="@+id/roomAvatarImageView"
android:layout_width="32dp"
@ -66,5 +60,3 @@
tools:text="115" />
</androidx.constraintlayout.widget.ConstraintLayout>
</im.vector.riotredesign.core.platform.CheckableFrameLayout>

View file

@ -1,11 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/sliding_menu_settings"
android:icon="@drawable/ic_settings"
android:title="@string/room_sliding_menu_settings" />
<item
android:id="@+id/sliding_menu_sign_out"
android:icon="@drawable/ic_material_exit_to_app"