diff --git a/changelog.d/6907.wip b/changelog.d/6907.wip new file mode 100644 index 0000000000..a8d887c66b --- /dev/null +++ b/changelog.d/6907.wip @@ -0,0 +1 @@ +[New Layout] Changes space sheet to accordion-style with expandable subspaces 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 index 7c4435bf59..2b45db2e4e 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryController.kt @@ -22,6 +22,7 @@ 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.extensions.orFalse 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 @@ -50,7 +51,8 @@ class NewSpaceSummaryController @Inject constructor( nonNullViewState.spaces, nonNullViewState.selectedSpace, nonNullViewState.rootSpacesOrdered, - nonNullViewState.homeAggregateCount + nonNullViewState.homeAggregateCount, + nonNullViewState.expandedStates, ) } @@ -58,24 +60,16 @@ class NewSpaceSummaryController @Inject constructor( spaceSummaries: List?, selectedSpace: RoomSummary?, rootSpaces: List?, - homeCount: RoomAggregateNotificationCount + homeCount: RoomAggregateNotificationCount, + expandedStates: Map, ) { - 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() } - } + addHomeItem(selectedSpace == null, homeCount) + addSpaces(spaceSummaries, selectedSpace, rootSpaces, expandedStates) + addCreateItem() } private fun addHomeItem(selected: Boolean, homeCount: RoomAggregateNotificationCount) { @@ -89,60 +83,95 @@ class NewSpaceSummaryController @Inject constructor( } } - private fun addSubSpaces( - selectedSpace: RoomSummary, + private fun addSpaces( spaceSummaries: List?, - homeCount: RoomAggregateNotificationCount, + selectedSpace: RoomSummary?, + rootSpaces: List?, + expandedStates: Map, ) { 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 + rootSpaces?.filter { it.membership != Membership.INVITE } + ?.forEach { spaceSummary -> + val subSpaces = spaceSummary.spaceChildren?.filter { spaceChild -> spaceSummaries.containsSpaceId(spaceChild.childRoomId) } + val hasChildren = (subSpaces?.size ?: 0) > 0 + val isSelected = spaceSummary.roomId == selectedSpace?.roomId + val expanded = expandedStates[spaceSummary.roomId] == true - 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 - ) - ) + newSpaceSummaryItem { + id(spaceSummary.roomId) + avatarRenderer(host.avatarRenderer) + countState(UnreadCounterBadgeView.State(spaceSummary.notificationCount, spaceSummary.highlightCount > 0)) + expanded(expanded) + hasChildren(hasChildren) + matrixItem(spaceSummary.toMatrixItem()) + onLongClickListener { host.callback?.onSpaceSettings(spaceSummary) } + onSpaceSelectedListener { host.callback?.onSpaceSelected(spaceSummary) } + onToggleExpandListener { host.callback?.onToggleExpand(spaceSummary) } + selected(isSelected) + } + + if (hasChildren && expanded) { + subSpaces?.forEach { child -> + addSubSpace(spaceSummary.roomId, spaceSummaries, expandedStates, selectedSpace, child, 1) + } + } } - } + } + + private fun List?.containsSpaceId(spaceId: String) = this?.any { it.roomId == spaceId }.orFalse() + + private fun addSubSpace( + idPrefix: String, + spaceSummaries: List?, + expandedStates: Map, + selectedSpace: RoomSummary?, + info: SpaceChildInfo, + depth: Int, + ) { + val host = this + val childSummary = spaceSummaries?.firstOrNull { it.roomId == info.childRoomId } ?: return + val id = "$idPrefix:${childSummary.roomId}" + val countState = UnreadCounterBadgeView.State(childSummary.notificationCount, childSummary.highlightCount > 0) + val expanded = expandedStates[childSummary.roomId] == true + val isSelected = childSummary.roomId == selectedSpace?.roomId + val subSpaces = childSummary.spaceChildren?.filter { childSpace -> spaceSummaries.containsSpaceId(childSpace.childRoomId) } + ?.sortedWith(subSpaceComparator) + + newSubSpaceSummaryItem { + id(id) + avatarRenderer(host.avatarRenderer) + countState(countState) + expanded(expanded) + hasChildren(!subSpaces.isNullOrEmpty()) + indent(depth) + matrixItem(childSummary.toMatrixItem()) + onLongClickListener { host.callback?.onSpaceSettings(childSummary) } + onSubSpaceSelectedListener { host.callback?.onSpaceSelected(childSummary) } + onToggleExpandListener { host.callback?.onToggleExpand(childSummary) } + selected(isSelected) } - if (!subSpacesAdded) { - addHomeItem(false, homeCount) + if (expanded) { + subSpaces?.forEach { + addSubSpace(id, spaceSummaries, expandedStates, selectedSpace, it, depth + 1) + } } } - private fun addRootSpaces(rootSpaces: List?) { + private fun addCreateItem() { 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)) - } - } + newSpaceAddItem { + id("create") + listener { host.callback?.onAddSpaceSelected() } + } } interface Callback { fun onSpaceSelected(spaceSummary: RoomSummary?) fun onSpaceInviteSelected(spaceSummary: RoomSummary) fun onSpaceSettings(spaceSummary: RoomSummary) + fun onToggleExpand(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 index 778b9c933e..f6a4781860 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryItem.kt @@ -18,6 +18,7 @@ package im.vector.app.features.spaces import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -34,16 +35,30 @@ import org.matrix.android.sdk.api.util.MatrixItem 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) + @EpoxyAttribute var expanded: Boolean = false + @EpoxyAttribute var hasChildren: Boolean = false + @EpoxyAttribute lateinit var matrixItem: MatrixItem + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onLongClickListener: ClickListener? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onSpaceSelectedListener: ClickListener? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onToggleExpandListener: ClickListener? = null + @EpoxyAttribute var selected: Boolean = false override fun bind(holder: Holder) { super.bind(holder) - holder.rootView.onClick(listener) + val context = holder.root.context + holder.root.onClick(onSpaceSelectedListener) + holder.root.setOnLongClickListener { + onLongClickListener?.invoke(holder.root) + true + } holder.name.text = matrixItem.displayName - holder.rootView.isChecked = selected + holder.root.isChecked = selected + + holder.chevron.setOnClickListener(onToggleExpandListener) + holder.chevron.isVisible = hasChildren + holder.chevron.setImageResource(if (expanded) R.drawable.ic_expand_more else R.drawable.ic_arrow_right) + holder.chevron.contentDescription = context.getString(if (expanded) R.string.a11y_collapse_space_children else R.string.a11y_expand_space_children) avatarRenderer.render(matrixItem, holder.avatar) holder.unreadCounter.render(countState) @@ -55,9 +70,10 @@ abstract class NewSpaceSummaryItem : VectorEpoxyModel(R.id.root) + 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) + val chevron by bind(R.id.chevron) } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/NewSubSpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/NewSubSpaceSummaryItem.kt new file mode 100644 index 0000000000..8dd2aea9b3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/NewSubSpaceSummaryItem.kt @@ -0,0 +1,89 @@ +/* + * 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.Space +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +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 NewSubSpaceSummaryItem : VectorEpoxyModel(R.layout.item_new_sub_space) { + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute var countState: UnreadCounterBadgeView.State = UnreadCounterBadgeView.State(0, false) + @EpoxyAttribute var expanded: Boolean = false + @EpoxyAttribute var hasChildren: Boolean = false + @EpoxyAttribute var indent: Int = 0 + @EpoxyAttribute lateinit var matrixItem: MatrixItem + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onLongClickListener: ClickListener? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onSubSpaceSelectedListener: ClickListener? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onToggleExpandListener: ClickListener? = null + @EpoxyAttribute var selected: Boolean = false + + override fun bind(holder: Holder) { + super.bind(holder) + holder.root.onClick(onSubSpaceSelectedListener) + holder.name.text = matrixItem.displayName + holder.root.isChecked = selected + holder.root.setOnLongClickListener { onLongClickListener?.invoke(holder.root).let { true } } + + holder.chevron.setImageDrawable( + ContextCompat.getDrawable( + holder.view.context, + if (expanded) R.drawable.ic_expand_more else R.drawable.ic_arrow_right + ) + ) + holder.chevron.onClick(onToggleExpandListener) + holder.chevron.isVisible = hasChildren + + holder.indent.isVisible = indent > 0 + holder.indent.updateLayoutParams { + width = indent * 30 + } + + avatarRenderer.render(matrixItem, holder.avatar) + holder.notificationBadge.render(countState) + } + + override fun unbind(holder: Holder) { + avatarRenderer.clear(holder.avatar) + super.unbind(holder) + } + + class Holder : VectorEpoxyHolder() { + val avatar by bind(R.id.avatar) + val name by bind(R.id.name) + val root by bind(R.id.root) + val chevron by bind(R.id.chevron) + val indent by bind(R.id.indent) + val notificationBadge by bind(R.id.notification_badge) + } +} 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 ca22ac30a1..ca9279cb37 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 @@ -77,7 +77,6 @@ class SpaceListFragment : private fun setupSpaceController() { if (vectorFeatures.isNewAppLayoutEnabled()) { - enableDragAndDropForNewSpaceController() newSpaceController.callback = this views.groupListView.configureWith(newSpaceController) } else { @@ -87,49 +86,6 @@ class SpaceListFragment : } } - 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) diff --git a/vector/src/main/res/layout/item_new_space.xml b/vector/src/main/res/layout/item_new_space.xml index 367d69ce69..fc023ebd6e 100644 --- a/vector/src/main/res/layout/item_new_space.xml +++ b/vector/src/main/res/layout/item_new_space.xml @@ -34,9 +34,9 @@ android:maxLines="1" android:textColor="?vctr_content_primary" android:textStyle="bold" - app:layout_constraintStart_toEndOf="@id/avatar" - app:layout_constraintEnd_toStartOf="@id/unread_counter" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/unread_counter" + app:layout_constraintStart_toEndOf="@id/avatar" app:layout_constraintTop_toTopOf="parent" tools:text="Element Corp" /> @@ -53,25 +53,28 @@ android:paddingEnd="4dp" android:textColor="?colorOnError" android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/chevron" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toBottomOf="parent" tools:background="@drawable/bg_unread_highlight" tools:text="147" tools:visibility="visible" /> + tools:ignore="MissingPrefix" + tools:src="@drawable/ic_arrow_right" + tools:visibility="visible" /> diff --git a/vector/src/main/res/layout/item_new_sub_space.xml b/vector/src/main/res/layout/item_new_sub_space.xml new file mode 100644 index 0000000000..014568e26d --- /dev/null +++ b/vector/src/main/res/layout/item_new_sub_space.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 5115510057..ad1df9b10e 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -141,6 +141,8 @@ Create Room Change Space Explore Rooms + Expand space children + Collapse space children