diff --git a/changelog.d/6749.wip b/changelog.d/6749.wip new file mode 100644 index 00000000000..d7fac1efc19 --- /dev/null +++ b/changelog.d/6749.wip @@ -0,0 +1 @@ +Adds space list bottom sheet for new app layout diff --git a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt index d90e934d0a8..c08f939524f 100644 --- a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt @@ -34,6 +34,7 @@ import im.vector.app.features.home.HomeSharedActionViewModel import im.vector.app.features.home.room.detail.RoomDetailSharedActionViewModel import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel +import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel import im.vector.app.features.reactions.EmojiChooserViewModel import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.app.features.roomprofile.RoomProfileSharedActionViewModel @@ -157,4 +158,9 @@ interface ViewModelModule { @IntoMap @ViewModelKey(SpacePeopleSharedActionViewModel::class) fun bindSpacePeopleSharedActionViewModel(viewModel: SpacePeopleSharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(RoomListSharedActionViewModel::class) + fun bindRoomListSharedActionViewModel(viewModel: RoomListSharedActionViewModel): ViewModel } diff --git a/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt index 816b9acb244..8ca217636a2 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt @@ -37,6 +37,7 @@ import im.vector.app.features.themes.ThemeUtils @EpoxyModelClass abstract class HomeSpaceSummaryItem : VectorEpoxyModel(R.layout.item_space) { + @EpoxyAttribute var text: String = "" @EpoxyAttribute var selected: Boolean = false @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null @EpoxyAttribute var countState: UnreadCounterBadgeView.State = UnreadCounterBadgeView.State(0, false) diff --git a/vector/src/main/java/im/vector/app/features/grouplist/NewHomeSpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/grouplist/NewHomeSpaceSummaryItem.kt new file mode 100644 index 00000000000..1f967db7ad5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/grouplist/NewHomeSpaceSummaryItem.kt @@ -0,0 +1,69 @@ +/* + * 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.app.features.grouplist + +import android.content.res.ColorStateList +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.platform.CheckableConstraintLayout +import im.vector.app.features.home.room.list.UnreadCounterBadgeView +import im.vector.app.features.themes.ThemeUtils + +@EpoxyModelClass +abstract class NewHomeSpaceSummaryItem : VectorEpoxyModel(R.layout.item_new_space) { + + @EpoxyAttribute var text: String = "" + @EpoxyAttribute var selected: Boolean = false + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null + @EpoxyAttribute var countState: UnreadCounterBadgeView.State = UnreadCounterBadgeView.State(0, false) + @EpoxyAttribute var showSeparator: Boolean = false + + override fun getViewType() = R.id.space_item_home + + override fun bind(holder: Holder) { + super.bind(holder) + holder.root.onClick(listener) + holder.name.text = holder.view.context.getString(R.string.all_chats) + holder.root.isChecked = selected + holder.root.context.resources + holder.avatar.background = ContextCompat.getDrawable(holder.view.context, R.drawable.new_space_home_background) + holder.avatar.backgroundTintList = ColorStateList.valueOf( + ColorUtils.setAlphaComponent(ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_tertiary), (255 * 0.3).toInt())) + holder.avatar.setImageResource(R.drawable.ic_space_home) + holder.avatar.imageTintList = ColorStateList.valueOf(ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_primary)) + holder.avatar.scaleType = ImageView.ScaleType.CENTER_INSIDE + + holder.unreadCounter.render(countState) + } + + class Holder : VectorEpoxyHolder() { + val root by bind(R.id.root) + val avatar by bind(R.id.avatar) + val name by bind(R.id.name) + val unreadCounter by bind(R.id.unread_counter) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt index 2e367480699..c2b61d694a8 100644 --- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt @@ -67,7 +67,7 @@ class NewHomeDetailFragment @Inject constructor( private val alertManager: PopupAlertManager, private val callManager: WebRtcCallManager, private val vectorPreferences: VectorPreferences, - private val appStateHandler: SpaceStateHandler, + private val spaceStateHandler: SpaceStateHandler, private val session: Session, ) : VectorBaseFragment(), KeysBackupBanner.Delegate, @@ -176,13 +176,13 @@ class NewHomeDetailFragment @Inject constructor( } private fun navigateBack() { - val previousSpaceId = appStateHandler.getSpaceBackstack().removeLastOrNull() - val parentSpaceId = appStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull() + val previousSpaceId = spaceStateHandler.getSpaceBackstack().removeLastOrNull() + val parentSpaceId = spaceStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull() setCurrentSpace(previousSpaceId ?: parentSpaceId) } private fun setCurrentSpace(spaceId: String?) { - appStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false) + spaceStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false) sharedActionViewModel.post(HomeActivitySharedAction.OnCloseSpace) } @@ -205,7 +205,7 @@ class NewHomeDetailFragment @Inject constructor( } private fun refreshSpaceState() { - appStateHandler.getCurrentSpace()?.let { + spaceStateHandler.getCurrentSpace()?.let { onSpaceChange(it) } } @@ -450,7 +450,7 @@ class NewHomeDetailFragment @Inject constructor( return this } - override fun onBackPressed(toolbarButton: Boolean) = if (appStateHandler.getCurrentSpace() != null) { + override fun onBackPressed(toolbarButton: Boolean) = if (spaceStateHandler.getCurrentSpace() != null) { navigateBack() true } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt index 0423a8fffcf..8c84aa55e1e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt @@ -25,8 +25,7 @@ sealed class RoomListQuickActionsSharedAction( @StringRes val titleRes: Int, @DrawableRes val iconResId: Int?, val destructive: Boolean = false -) : - VectorSharedAction { +) : VectorSharedAction { data class NotificationsAllNoisy(val roomId: String) : RoomListQuickActionsSharedAction( R.string.room_list_quick_actions_notifications_all_noisy, diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListSharedAction.kt new file mode 100644 index 00000000000..766bc6eea1b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListSharedAction.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 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.app.features.home.room.list.actions + +import im.vector.app.core.platform.VectorSharedAction + +sealed class RoomListSharedAction : VectorSharedAction { + + object CloseBottomSheet : RoomListSharedAction() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListSharedActionViewModel.kt new file mode 100644 index 00000000000..e2545accc8c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListSharedActionViewModel.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 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.app.features.home.room.list.actions + +import im.vector.app.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +class RoomListSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt index c98d22a34f7..8cbf760025e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt @@ -43,9 +43,12 @@ import im.vector.app.features.home.room.list.RoomSummaryItemFactory import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel +import im.vector.app.features.home.room.list.actions.RoomListSharedAction +import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel import im.vector.app.features.home.room.list.home.filter.HomeFilteredRoomsController import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter import im.vector.app.features.home.room.list.home.recent.RecentRoomCarouselController +import im.vector.app.features.spaces.SpaceListBottomSheet import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -62,10 +65,13 @@ class HomeRoomListFragment @Inject constructor( RoomListListener { private val roomListViewModel: HomeRoomListViewModel by fragmentViewModel() - private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel + private lateinit var sharedQuickActionsViewModel: RoomListQuickActionsSharedActionViewModel + private lateinit var sharedActionViewModel: RoomListSharedActionViewModel private var concatAdapter = ConcatAdapter() private var modelBuildListener: OnModelBuildFinishedListener? = null + private val spaceListBottomSheet = SpaceListBottomSheet() + private lateinit var stateRestorer: LayoutManagerStateRestorer override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomListBinding { @@ -74,15 +80,25 @@ class HomeRoomListFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + views.stateView.contentView = views.roomListView + views.stateView.state = StateView.State.Loading + setupObservers() + setupRecyclerView() + setupFabs() + } + + private fun setupObservers() { + sharedQuickActionsViewModel = activityViewModelProvider[RoomListQuickActionsSharedActionViewModel::class.java] + sharedActionViewModel = activityViewModelProvider[RoomListSharedActionViewModel::class.java] - sharedActionViewModel = activityViewModelProvider[RoomListQuickActionsSharedActionViewModel::class.java] sharedActionViewModel .stream() - .onEach { handleQuickActions(it) } + .onEach(::handleSharedAction) + .launchIn(viewLifecycleOwner.lifecycleScope) + sharedQuickActionsViewModel + .stream() + .onEach(::handleQuickActions) .launchIn(viewLifecycleOwner.lifecycleScope) - - views.stateView.contentView = views.roomListView - views.stateView.state = StateView.State.Loading roomListViewModel.observeViewEvents { when (it) { @@ -92,9 +108,42 @@ class HomeRoomListFragment @Inject constructor( is HomeRoomListViewEvents.Done -> Unit } } + } - setupRecyclerView() - setupFabs() + private fun handleSharedAction(action: RoomListSharedAction) { + when (action) { + RoomListSharedAction.CloseBottomSheet -> spaceListBottomSheet.dismiss() + } + } + + private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) { + when (quickAction) { + is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> { + roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES_NOISY)) + } + is RoomListQuickActionsSharedAction.NotificationsAll -> { + roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES)) + } + is RoomListQuickActionsSharedAction.NotificationsMentionsOnly -> { + roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.MENTIONS_ONLY)) + } + is RoomListQuickActionsSharedAction.NotificationsMute -> { + roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.MUTE)) + } + is RoomListQuickActionsSharedAction.Settings -> { + navigator.openRoomProfile(requireActivity(), quickAction.roomId) + } + is RoomListQuickActionsSharedAction.Favorite -> { + roomListViewModel.handle(HomeRoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_FAVOURITE)) + } + is RoomListQuickActionsSharedAction.LowPriority -> { + roomListViewModel.handle(HomeRoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_LOW_PRIORITY)) + } + is RoomListQuickActionsSharedAction.Leave -> { + roomListViewModel.handle(HomeRoomListAction.LeaveRoom(quickAction.roomId)) + promptLeaveRoom(quickAction.roomId) + } + } } private fun setupRecyclerView() { @@ -121,7 +170,8 @@ class HomeRoomListFragment @Inject constructor( } views.newLayoutOpenSpacesButton.setOnClickListener { - // Click action for open spaces modal goes here (Issue #6499) + // Click action for open spaces modal goes here + spaceListBottomSheet.show(requireActivity().supportFragmentManager, SpaceListBottomSheet.TAG) } // Hide FABs when list is scrolling @@ -158,36 +208,6 @@ class HomeRoomListFragment @Inject constructor( } } - private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) { - when (quickAction) { - is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> { - roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES_NOISY)) - } - is RoomListQuickActionsSharedAction.NotificationsAll -> { - roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES)) - } - is RoomListQuickActionsSharedAction.NotificationsMentionsOnly -> { - roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.MENTIONS_ONLY)) - } - is RoomListQuickActionsSharedAction.NotificationsMute -> { - roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.MUTE)) - } - is RoomListQuickActionsSharedAction.Settings -> { - navigator.openRoomProfile(requireActivity(), quickAction.roomId) - } - is RoomListQuickActionsSharedAction.Favorite -> { - roomListViewModel.handle(HomeRoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_FAVOURITE)) - } - is RoomListQuickActionsSharedAction.LowPriority -> { - roomListViewModel.handle(HomeRoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_LOW_PRIORITY)) - } - is RoomListQuickActionsSharedAction.Leave -> { - roomListViewModel.handle(HomeRoomListAction.LeaveRoom(quickAction.roomId)) - promptLeaveRoom(quickAction.roomId) - } - } - } - private fun promptLeaveRoom(roomId: String) { val isPublicRoom = roomListViewModel.isPublicRoom(roomId) val message = buildString { diff --git a/vector/src/main/java/im/vector/app/features/spaces/NewSpaceAddItem.kt b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceAddItem.kt new file mode 100644 index 00000000000..60816df9c05 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceAddItem.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 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.app.features.spaces + +import android.content.res.ColorStateList +import android.widget.ImageView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick +import im.vector.app.features.themes.ThemeUtils + +@EpoxyModelClass +abstract class NewSpaceAddItem : VectorEpoxyModel(R.layout.item_new_space_add) { + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.view.onClick(listener) + + holder.plus.imageTintList = ColorStateList.valueOf(ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_primary)) + } + + class Holder : VectorEpoxyHolder() { + val plus by bind(R.id.plus) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/NewSpaceListHeaderItem.kt b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceListHeaderItem.kt new file mode 100644 index 00000000000..8fc53f07d47 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceListHeaderItem.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 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.app.features.spaces + +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass +abstract class NewSpaceListHeaderItem : VectorEpoxyModel(R.layout.item_new_space_list_header) { + class Holder : VectorEpoxyHolder() +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryController.kt b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryController.kt new file mode 100644 index 00000000000..7c4435bf591 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryController.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2021 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.app.features.spaces + +import com.airbnb.epoxy.EpoxyController +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.grouplist.newHomeSpaceSummaryItem +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.list.UnreadCounterBadgeView +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class NewSpaceSummaryController @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider, +) : EpoxyController() { + + var callback: Callback? = null + private var viewState: SpaceListViewState? = null + + private val subSpaceComparator: Comparator = compareBy { it.order }.thenBy { it.childRoomId } + + fun update(viewState: SpaceListViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val nonNullViewState = viewState ?: return + buildGroupModels( + nonNullViewState.spaces, + nonNullViewState.selectedSpace, + nonNullViewState.rootSpacesOrdered, + nonNullViewState.homeAggregateCount + ) + } + + private fun buildGroupModels( + spaceSummaries: List?, + selectedSpace: RoomSummary?, + rootSpaces: List?, + homeCount: RoomAggregateNotificationCount + ) { + val host = this + newSpaceListHeaderItem { + id("space_list_header") + } + + if (selectedSpace != null) { + addSubSpaces(selectedSpace, spaceSummaries, homeCount) + } else { + addHomeItem(true, homeCount) + addRootSpaces(rootSpaces) + } + + newSpaceAddItem { + id("create") + listener { host.callback?.onAddSpaceSelected() } + } + } + + private fun addHomeItem(selected: Boolean, homeCount: RoomAggregateNotificationCount) { + val host = this + newHomeSpaceSummaryItem { + id("space_home") + text(host.stringProvider.getString(R.string.all_chats)) + selected(selected) + countState(UnreadCounterBadgeView.State(homeCount.totalCount, homeCount.isHighlight)) + listener { host.callback?.onSpaceSelected(null) } + } + } + + private fun addSubSpaces( + selectedSpace: RoomSummary, + spaceSummaries: List?, + homeCount: RoomAggregateNotificationCount, + ) { + val host = this + val spaceChildren = selectedSpace.spaceChildren + var subSpacesAdded = false + + spaceChildren?.sortedWith(subSpaceComparator)?.forEach { spaceChild -> + val subSpaceSummary = spaceSummaries?.firstOrNull { it.roomId == spaceChild.childRoomId } ?: return@forEach + + if (subSpaceSummary.membership != Membership.INVITE) { + subSpacesAdded = true + newSpaceSummaryItem { + avatarRenderer(host.avatarRenderer) + id(subSpaceSummary.roomId) + matrixItem(subSpaceSummary.toMatrixItem()) + selected(false) + listener { host.callback?.onSpaceSelected(subSpaceSummary) } + countState( + UnreadCounterBadgeView.State( + subSpaceSummary.notificationCount, + subSpaceSummary.highlightCount > 0 + ) + ) + } + } + } + + if (!subSpacesAdded) { + addHomeItem(false, homeCount) + } + } + + private fun addRootSpaces(rootSpaces: List?) { + val host = this + rootSpaces + ?.filter { it.membership != Membership.INVITE } + ?.forEach { roomSummary -> + newSpaceSummaryItem { + avatarRenderer(host.avatarRenderer) + id(roomSummary.roomId) + matrixItem(roomSummary.toMatrixItem()) + listener { host.callback?.onSpaceSelected(roomSummary) } + countState(UnreadCounterBadgeView.State(roomSummary.notificationCount, roomSummary.highlightCount > 0)) + } + } + } + + interface Callback { + fun onSpaceSelected(spaceSummary: RoomSummary?) + fun onSpaceInviteSelected(spaceSummary: RoomSummary) + fun onSpaceSettings(spaceSummary: RoomSummary) + fun onAddSpaceSelected() + fun sendFeedBack() + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryItem.kt new file mode 100644 index 00000000000..778b9c933e9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryItem.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2021 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.app.features.spaces + +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.platform.CheckableConstraintLayout +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.list.UnreadCounterBadgeView +import org.matrix.android.sdk.api.util.MatrixItem + +@EpoxyModelClass +abstract class NewSpaceSummaryItem : VectorEpoxyModel(R.layout.item_new_space) { + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute lateinit var matrixItem: MatrixItem + @EpoxyAttribute var selected: Boolean = false + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null + @EpoxyAttribute var countState: UnreadCounterBadgeView.State = UnreadCounterBadgeView.State(0, false) + + override fun bind(holder: Holder) { + super.bind(holder) + holder.rootView.onClick(listener) + holder.name.text = matrixItem.displayName + holder.rootView.isChecked = selected + + avatarRenderer.render(matrixItem, holder.avatar) + holder.unreadCounter.render(countState) + } + + override fun unbind(holder: Holder) { + avatarRenderer.clear(holder.avatar) + super.unbind(holder) + } + + class Holder : VectorEpoxyHolder() { + val rootView by bind(R.id.root) + val avatar by bind(R.id.avatar) + val name by bind(R.id.name) + val unreadCounter by bind(R.id.unread_counter) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListBottomSheet.kt new file mode 100644 index 00000000000..910f8c5379d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListBottomSheet.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 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.app.features.spaces + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import im.vector.app.R +import im.vector.app.core.extensions.replaceChildFragment +import im.vector.app.databinding.FragmentSpacesBottomSheetBinding + +class SpaceListBottomSheet : BottomSheetDialogFragment() { + + private lateinit var binding: FragmentSpacesBottomSheetBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentSpacesBottomSheetBinding.inflate(inflater, container, false) + if (savedInstanceState == null) { + replaceChildFragment(R.id.space_list, SpaceListFragment::class.java) + } + return binding.root + } + + companion object { + const val TAG = "SpacesBottomSheet" + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt index b358a8c1a6a..7b034356b49 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt @@ -32,20 +32,29 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSpaceListBinding +import im.vector.app.features.VectorFeatures import im.vector.app.features.home.HomeActivitySharedAction import im.vector.app.features.home.HomeSharedActionViewModel +import im.vector.app.features.home.room.list.actions.RoomListSharedAction +import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel import org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject /** * This Fragment is displayed in the navigation drawer [im.vector.app.features.home.HomeDrawerFragment] and * is displaying the space hierarchy, with some actions on Spaces. + * + * In the New App Layout this fragment will instead be displayed in a Bottom Sheet [SpaceListBottomSheet] + * and will only display spaces that are direct children of the currently selected space (or root spaces if none) */ class SpaceListFragment @Inject constructor( - private val spaceController: SpaceSummaryController -) : VectorBaseFragment(), SpaceSummaryController.Callback { + private val spaceController: SpaceSummaryController, + private val newSpaceController: NewSpaceSummaryController, + private val vectorFeatures: VectorFeatures, +) : VectorBaseFragment(), SpaceSummaryController.Callback, NewSpaceSummaryController.Callback { - private lateinit var sharedActionViewModel: HomeSharedActionViewModel + private lateinit var homeActivitySharedActionViewModel: HomeSharedActionViewModel + private lateinit var roomListSharedActionViewModel: RoomListSharedActionViewModel private val viewModel: SpaceListViewModel by fragmentViewModel() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSpaceListBinding { @@ -54,10 +63,69 @@ class SpaceListFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java) - spaceController.callback = this + homeActivitySharedActionViewModel = activityViewModelProvider[HomeSharedActionViewModel::class.java] + roomListSharedActionViewModel = activityViewModelProvider[RoomListSharedActionViewModel::class.java] views.stateView.contentView = views.groupListView - views.groupListView.configureWith(spaceController) + setupSpaceController() + observeViewEvents() + } + + private fun setupSpaceController() { + if (vectorFeatures.isNewAppLayoutEnabled()) { + enableDragAndDropForNewSpaceController() + newSpaceController.callback = this + views.groupListView.configureWith(newSpaceController) + } else { + enableDragAndDropForSpaceController() + spaceController.callback = this + views.groupListView.configureWith(spaceController) + } + } + + private fun enableDragAndDropForNewSpaceController() { + EpoxyTouchHelper.initDragging(newSpaceController) + .withRecyclerView(views.groupListView) + .forVerticalList() + .withTarget(NewSpaceSummaryItem::class.java) + .andCallbacks(object : EpoxyTouchHelper.DragCallbacks() { + var toPositionM: Int? = null + var fromPositionM: Int? = null + var initialElevation: Float? = null + + override fun onDragStarted(model: NewSpaceSummaryItem?, itemView: View?, adapterPosition: Int) { + toPositionM = null + fromPositionM = null + model?.matrixItem?.id?.let { + viewModel.handle(SpaceListAction.OnStartDragging(it, false)) + } + itemView?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + initialElevation = itemView?.elevation + itemView?.elevation = 6f + } + + override fun onDragReleased(model: NewSpaceSummaryItem?, itemView: View?) { + if (toPositionM == null || fromPositionM == null) return + val movedSpaceId = model?.matrixItem?.id ?: return + viewModel.handle(SpaceListAction.MoveSpace(movedSpaceId, toPositionM!! - fromPositionM!!)) + } + + override fun clearView(model: NewSpaceSummaryItem?, itemView: View?) { + itemView?.elevation = initialElevation ?: 0f + } + + override fun onModelMoved(fromPosition: Int, toPosition: Int, modelBeingMoved: NewSpaceSummaryItem?, itemView: View?) { + if (fromPositionM == null) { + fromPositionM = fromPosition + } + if (toPositionM != toPosition) { + toPositionM = toPosition + itemView?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } + } + }) + } + + private fun enableDragAndDropForSpaceController() { EpoxyTouchHelper.initDragging(spaceController) .withRecyclerView(views.groupListView) .forVerticalList() @@ -100,14 +168,14 @@ class SpaceListFragment @Inject constructor( return model?.canDrag == true } }) + } - viewModel.observeViewEvents { - when (it) { - is SpaceListViewEvents.OpenSpaceSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenSpacePreview(it.id)) - is SpaceListViewEvents.AddSpace -> sharedActionViewModel.post(HomeActivitySharedAction.AddSpace) - is SpaceListViewEvents.OpenSpaceInvite -> sharedActionViewModel.post(HomeActivitySharedAction.OpenSpaceInvite(it.id)) - SpaceListViewEvents.CloseDrawer -> sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer) - } + private fun observeViewEvents() = viewModel.observeViewEvents { + when (it) { + is SpaceListViewEvents.OpenSpaceSummary -> homeActivitySharedActionViewModel.post(HomeActivitySharedAction.OpenSpacePreview(it.id)) + is SpaceListViewEvents.AddSpace -> homeActivitySharedActionViewModel.post(HomeActivitySharedAction.AddSpace) + is SpaceListViewEvents.OpenSpaceInvite -> homeActivitySharedActionViewModel.post(HomeActivitySharedAction.OpenSpaceInvite(it.id)) + SpaceListViewEvents.CloseDrawer -> homeActivitySharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer) } } @@ -124,11 +192,17 @@ class SpaceListFragment @Inject constructor( is Success -> views.stateView.state = StateView.State.Content else -> Unit } - spaceController.update(state) + + if (vectorFeatures.isNewAppLayoutEnabled()) { + newSpaceController.update(state) + } else { + spaceController.update(state) + } } override fun onSpaceSelected(spaceSummary: RoomSummary?) { viewModel.handle(SpaceListAction.SelectSpace(spaceSummary)) + roomListSharedActionViewModel.post(RoomListSharedAction.CloseBottomSheet) } override fun onSpaceInviteSelected(spaceSummary: RoomSummary) { @@ -136,7 +210,7 @@ class SpaceListFragment @Inject constructor( } override fun onSpaceSettings(spaceSummary: RoomSummary) { - sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(spaceSummary.roomId)) + homeActivitySharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(spaceSummary.roomId)) } override fun onToggleExpand(spaceSummary: RoomSummary) { @@ -148,6 +222,6 @@ class SpaceListFragment @Inject constructor( } override fun sendFeedBack() { - sharedActionViewModel.post(HomeActivitySharedAction.SendSpaceFeedBack) + homeActivitySharedActionViewModel.post(HomeActivitySharedAction.SendSpaceFeedBack) } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt index eea11f9b1b9..9048026771c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt @@ -194,7 +194,7 @@ class SpaceListViewModel @AssistedInject constructor( val moved = removeAt(index) add(index + action.delta, moved) }, - spaceOrderLocalEchos = updatedLocalEchos + spaceOrderLocalEchos = updatedLocalEchos, ) } session.coroutineScope.launch { @@ -257,29 +257,29 @@ class SpaceListViewModel @AssistedInject constructor( } combine( - session.flow() - .liveSpaceSummaries(params), + session.flow().liveSpaceSummaries(params), session.accountDataService() .getLiveRoomAccountDataEvents(setOf(RoomAccountDataTypes.EVENT_TYPE_SPACE_ORDER)) .asFlow() ) { spaces, _ -> spaces + }.execute { asyncSpaces -> + val spaces = asyncSpaces.invoke().orEmpty() + val rootSpaces = asyncSpaces.invoke().orEmpty().filter { it.flattenParentIds.isEmpty() } + val orders = rootSpaces.associate { + it.roomId to session.getRoom(it.roomId) + ?.roomAccountDataService() + ?.getAccountDataEvent(RoomAccountDataTypes.EVENT_TYPE_SPACE_ORDER) + ?.content.toModel() + ?.safeOrder() + } + copy( + asyncSpaces = asyncSpaces, + spaces = spaces, + rootSpacesOrdered = rootSpaces.sortedWith(TopLevelSpaceComparator(orders)), + spaceOrderInfo = orders + ) } - .execute { async -> - val rootSpaces = async.invoke().orEmpty().filter { it.flattenParentIds.isEmpty() } - val orders = rootSpaces.associate { - it.roomId to session.getRoom(it.roomId) - ?.roomAccountDataService() - ?.getAccountDataEvent(RoomAccountDataTypes.EVENT_TYPE_SPACE_ORDER) - ?.content.toModel() - ?.safeOrder() - } - copy( - asyncSpaces = async, - rootSpacesOrdered = rootSpaces.sortedWith(TopLevelSpaceComparator(orders)), - spaceOrderInfo = orders - ) - } // clear local echos on update session.accountDataService() diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewState.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewState.kt index 794f1dbd69f..f75c336b5de 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewState.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.util.MatrixItem data class SpaceListViewState( val myMxItem: Async = Uninitialized, val asyncSpaces: Async> = Uninitialized, + val spaces: List = emptyList(), val selectedSpace: RoomSummary? = null, val rootSpacesOrdered: List? = null, val spaceOrderInfo: Map? = null, diff --git a/vector/src/main/res/drawable/ic_plus.xml b/vector/src/main/res/drawable/ic_plus.xml new file mode 100644 index 00000000000..25a611472b3 --- /dev/null +++ b/vector/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,11 @@ + + + + diff --git a/vector/src/main/res/drawable/new_space_home_background.xml b/vector/src/main/res/drawable/new_space_home_background.xml new file mode 100644 index 00000000000..47fdeb02264 --- /dev/null +++ b/vector/src/main/res/drawable/new_space_home_background.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/vector/src/main/res/drawable/space_home_background.xml b/vector/src/main/res/drawable/space_home_background.xml index 2efd6d407cf..c7fc727267f 100644 --- a/vector/src/main/res/drawable/space_home_background.xml +++ b/vector/src/main/res/drawable/space_home_background.xml @@ -10,4 +10,4 @@ - \ No newline at end of file + diff --git a/vector/src/main/res/layout/fragment_room_list.xml b/vector/src/main/res/layout/fragment_room_list.xml index 17d93eb98a5..4b6f8f5895b 100644 --- a/vector/src/main/res/layout/fragment_room_list.xml +++ b/vector/src/main/res/layout/fragment_room_list.xml @@ -1,101 +1,107 @@ - + android:layout_height="match_parent"> - + android:background="?android:colorBackground"> - - - - - + - + - + app:maxImageSize="34dp" + tools:layout_marginEnd="80dp" + tools:visibility="visible" /> + app:maxImageSize="32dp" + tools:layout_marginEnd="144dp" + tools:visibility="visible" /> + + + + + + + + + - + - + diff --git a/vector/src/main/res/layout/fragment_spaces_bottom_sheet.xml b/vector/src/main/res/layout/fragment_spaces_bottom_sheet.xml new file mode 100644 index 00000000000..27324c852f8 --- /dev/null +++ b/vector/src/main/res/layout/fragment_spaces_bottom_sheet.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/vector/src/main/res/layout/item_new_space.xml b/vector/src/main/res/layout/item_new_space.xml new file mode 100644 index 00000000000..367d69ce692 --- /dev/null +++ b/vector/src/main/res/layout/item_new_space.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_new_space_add.xml b/vector/src/main/res/layout/item_new_space_add.xml new file mode 100644 index 00000000000..5a62abd740f --- /dev/null +++ b/vector/src/main/res/layout/item_new_space_add.xml @@ -0,0 +1,54 @@ + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_new_space_list_header.xml b/vector/src/main/res/layout/item_new_space_list_header.xml new file mode 100644 index 00000000000..2c52304249e --- /dev/null +++ b/vector/src/main/res/layout/item_new_space_list_header.xml @@ -0,0 +1,16 @@ + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 082d936806a..56188fd2ac1 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -137,6 +137,7 @@ All Chats + Change Space