diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 208246aa68..1a7a07bee2 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -33,6 +33,7 @@ import im.vector.riotx.features.home.LoadingFragment import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsersFragment import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment import im.vector.riotx.features.home.group.GroupListFragment +import im.vector.riotx.features.home.room.breadcrumbs.BreadcrumbsFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.login.* @@ -249,4 +250,9 @@ interface FragmentModule { @IntoMap @FragmentKey(PublicRoomsFragment::class) fun bindPublicRoomsFragment(fragment: PublicRoomsFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(BreadcrumbsFragment::class) + fun bindBreadcrumbsFragment(fragment: BreadcrumbsFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 0876701504..4b136c557b 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -29,6 +29,7 @@ import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedVie import im.vector.riotx.features.crypto.verification.SasVerificationViewModel import im.vector.riotx.features.home.HomeSharedActionViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomSharedActionViewModel +import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.riotx.features.login.LoginSharedActionViewModel @@ -118,4 +119,9 @@ interface ViewModelModule { @IntoMap @ViewModelKey(LoginSharedActionViewModel::class) fun bindLoginSharedActionViewModel(viewModel: LoginSharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(RoomDetailSharedActionViewModel::class) + fun bindRoomDetailSharedActionViewModel(viewModel: RoomDetailSharedActionViewModel): ViewModel } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsAnimator.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsAnimator.kt new file mode 100644 index 0000000000..2e849dfe38 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsAnimator.kt @@ -0,0 +1,31 @@ +/* + * 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.riotx.features.home.room.breadcrumbs + +import androidx.recyclerview.widget.DefaultItemAnimator + +private const val ANIM_DURATION_IN_MILLIS = 200L + +class BreadcrumbsAnimator : DefaultItemAnimator() { + + init { + addDuration = ANIM_DURATION_IN_MILLIS + removeDuration = ANIM_DURATION_IN_MILLIS + moveDuration = ANIM_DURATION_IN_MILLIS + changeDuration = ANIM_DURATION_IN_MILLIS + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt new file mode 100644 index 0000000000..eb97b91cbe --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt @@ -0,0 +1,70 @@ +/* + * 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.riotx.features.home.room.breadcrumbs + +import android.view.View +import com.airbnb.epoxy.EpoxyController +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.riotx.core.utils.DebouncedClickListener +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +class BreadcrumbsController @Inject constructor( + private val avatarRenderer: AvatarRenderer +) : EpoxyController() { + + var listener: Listener? = null + + private var viewState: BreadcrumbsViewState? = null + + init { + // We are requesting a model build directly as the first build of epoxy is on the main thread. + // It avoids to build the the whole list of rooms on the main thread. + requestModelBuild() + } + + fun update(viewState: BreadcrumbsViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val nonNullViewState = viewState ?: return + + // TODO Display a loading, or an empty state + + nonNullViewState.asyncRooms.invoke() + ?.forEach { + breadcrumbsItem { + id(it.roomId) + avatarRenderer(avatarRenderer) + roomId(it.roomId) + roomName(it.displayName) + avatarUrl(it.avatarUrl) + itemClickListener( + DebouncedClickListener(View.OnClickListener { _ -> + listener?.onRoomClicked(it) + }) + ) + } + } + } + + interface Listener { + fun onRoomClicked(room: RoomSummary) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt new file mode 100644 index 0000000000..036208ec6a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt @@ -0,0 +1,71 @@ +/* + * 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.riotx.features.home.room.breadcrumbs + +import android.os.Bundle +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import com.airbnb.mvrx.fragmentViewModel +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.home.room.detail.RoomDetailSharedAction +import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel +import kotlinx.android.synthetic.main.fragment_breadcrumbs.* +import javax.inject.Inject + +class BreadcrumbsFragment @Inject constructor( + private val breadcrumbsController: BreadcrumbsController, + val breadcrumbsViewModelFactory: BreadcrumbsViewModel.Factory +) : VectorBaseFragment(), BreadcrumbsController.Listener { + + private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel + private val breadcrumbsViewModel: BreadcrumbsViewModel by fragmentViewModel() + + override fun getLayoutResId() = R.layout.fragment_breadcrumbs + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + sharedActionViewModel = activityViewModelProvider.get(RoomDetailSharedActionViewModel::class.java) + + breadcrumbsViewModel.subscribe { renderState(it) } + } + + override fun onDestroyView() { + super.onDestroyView() + breadcrumbsRecyclerView.adapter = null + } + + private fun setupRecyclerView() { + val layoutManager = LinearLayoutManager(context) + breadcrumbsRecyclerView.layoutManager = layoutManager + breadcrumbsRecyclerView.itemAnimator = BreadcrumbsAnimator() + breadcrumbsController.listener = this + breadcrumbsRecyclerView.setController(breadcrumbsController) + } + + private fun renderState(state: BreadcrumbsViewState) { + breadcrumbsController.update(state) + } + + // BreadcrumbsController.Listener ************************************************************** + + override fun onRoomClicked(room: RoomSummary) { + sharedActionViewModel.post(RoomDetailSharedAction.OpenRoom(room.roomId)) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt new file mode 100644 index 0000000000..d8d1acb6d3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt @@ -0,0 +1,53 @@ +/* + * 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.riotx.features.home.room.breadcrumbs + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.features.home.AvatarRenderer + +@EpoxyModelClass(layout = R.layout.item_breadcrumbs) +abstract class BreadcrumbsItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute lateinit var roomId: String + @EpoxyAttribute lateinit var roomName: CharSequence + @EpoxyAttribute var avatarUrl: String? = null + // TODO @EpoxyAttribute var unreadNotificationCount: Int = 0 + // TODO @EpoxyAttribute var hasUnreadMessage: Boolean = false + // TODO @EpoxyAttribute var showHighlighted: Boolean = false + @EpoxyAttribute var itemClickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.rootView.setOnClickListener(itemClickListener) + avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView) + } + + class Holder : VectorEpoxyHolder() { + // TODO val unreadCounterBadgeView by bind(R.id.roomUnreadCounterBadgeView) + // TODO val unreadIndentIndicator by bind(R.id.roomUnreadIndicator) + val avatarImageView by bind(R.id.breadcrumbsImageView) + val rootView by bind(R.id.breadcrumbsRoot) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt new file mode 100644 index 0000000000..4b462a05a6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt @@ -0,0 +1,66 @@ +/* + * 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.riotx.features.home.room.breadcrumbs + +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.rx.rx +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.VectorViewModel +import io.reactivex.schedulers.Schedulers + +class BreadcrumbsViewModel @AssistedInject constructor(@Assisted initialState: BreadcrumbsViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: BreadcrumbsViewState): BreadcrumbsViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: BreadcrumbsViewState): BreadcrumbsViewModel? { + val fragment: BreadcrumbsFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.breadcrumbsViewModelFactory.create(state) + } + } + + init { + observeBreadcrumbs() + } + + override fun handle(action: EmptyAction) { + // No op + } + + // PRIVATE METHODS ***************************************************************************** + + private fun observeBreadcrumbs() { + session.rx() + .liveBreadcrumbs() + .observeOn(Schedulers.computation()) + .execute { asyncRooms -> + copy(asyncRooms = asyncRooms) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewState.kt new file mode 100644 index 0000000000..cb00db4c9f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewState.kt @@ -0,0 +1,26 @@ +/* + * 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.riotx.features.home.room.breadcrumbs + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.room.model.RoomSummary + +data class BreadcrumbsViewState( + val asyncRooms: Async> = Uninitialized +) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt index eb8118a0c9..bdc3b59eff 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt @@ -20,17 +20,25 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.appcompat.widget.Toolbar +import androidx.core.view.GravityCompat +import androidx.drawerlayout.widget.DrawerLayout import im.vector.riotx.R +import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.home.room.breadcrumbs.BreadcrumbsFragment +import kotlinx.android.synthetic.main.activity_room_detail.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { - override fun getLayoutRes(): Int { - return R.layout.activity_room_detail - } + override fun getLayoutRes() = R.layout.activity_room_detail + + private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel + + // Simple filter + private var currentRoomId: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -38,14 +46,55 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { if (isFirstCreation()) { val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) ?: return + currentRoomId = roomDetailArgs.roomId replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, roomDetailArgs) + replaceFragment(R.id.roomDetailDrawerContainer, BreadcrumbsFragment::class.java) } + + sharedActionViewModel = viewModelProvider.get(RoomDetailSharedActionViewModel::class.java) + + sharedActionViewModel + .observe() + .subscribe { sharedAction -> + when (sharedAction) { + is RoomDetailSharedAction.OpenRoom -> { + drawerLayout.closeDrawer(GravityCompat.START) + // Do not replace the Fragment if it's the same roomId + if (currentRoomId != sharedAction.roomId) { + currentRoomId = sharedAction.roomId + replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, RoomDetailArgs(sharedAction.roomId)) + } + } + } + } + .disposeOnDestroy() + + drawerLayout.addDrawerListener(drawerListener) + } + + override fun onDestroy() { + drawerLayout.removeDrawerListener(drawerListener) + super.onDestroy() } override fun configure(toolbar: Toolbar) { configureToolbar(toolbar) } + private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { + override fun onDrawerStateChanged(newState: Int) { + hideKeyboard() + } + } + + override fun onBackPressed() { + if (drawerLayout.isDrawerOpen(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START) + } else { + super.onBackPressed() + } + } + companion object { private const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS" diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailSharedAction.kt new file mode 100644 index 0000000000..6a88bb1f13 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailSharedAction.kt @@ -0,0 +1,26 @@ +/* + * 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.riotx.features.home.room.detail + +import im.vector.riotx.core.platform.VectorSharedAction + +/** + * Supported navigation actions for [RoomDetailActivity] + */ +sealed class RoomDetailSharedAction : VectorSharedAction { + data class OpenRoom(val roomId: String) : RoomDetailSharedAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailSharedActionViewModel.kt new file mode 100644 index 0000000000..6f2162bebc --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailSharedActionViewModel.kt @@ -0,0 +1,24 @@ +/* + * 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.riotx.features.home.room.detail + +import im.vector.riotx.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +/** + * Activity shared view model + */ +class RoomDetailSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/res/layout/activity_home.xml b/vector/src/main/res/layout/activity_home.xml index 0ce124b787..61fb1b5ad4 100644 --- a/vector/src/main/res/layout/activity_home.xml +++ b/vector/src/main/res/layout/activity_home.xml @@ -17,6 +17,7 @@ android:layout_height="match_parent" /> + diff --git a/vector/src/main/res/layout/activity_room_detail.xml b/vector/src/main/res/layout/activity_room_detail.xml index 4d8fc23f24..14fda5e9c6 100644 --- a/vector/src/main/res/layout/activity_room_detail.xml +++ b/vector/src/main/res/layout/activity_room_detail.xml @@ -1,14 +1,36 @@ - - + android:layout_height="match_parent" + tools:openDrawer="start"> + + + + + + + + + + + + + + android:layout_height="match_parent" + android:layout_gravity="start" /> - - - \ No newline at end of file + diff --git a/vector/src/main/res/layout/fragment_breadcrumbs.xml b/vector/src/main/res/layout/fragment_breadcrumbs.xml new file mode 100644 index 0000000000..780a1a5cb2 --- /dev/null +++ b/vector/src/main/res/layout/fragment_breadcrumbs.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_breadcrumbs.xml b/vector/src/main/res/layout/item_breadcrumbs.xml new file mode 100644 index 0000000000..0f8b535b7b --- /dev/null +++ b/vector/src/main/res/layout/item_breadcrumbs.xml @@ -0,0 +1,22 @@ + + + + + +