1 depth hierarchy support in space panel

This commit is contained in:
Valere 2021-03-22 11:40:28 +01:00
parent 872b383c4d
commit ea5e48b940
9 changed files with 217 additions and 55 deletions

View File

@ -370,4 +370,44 @@ class SpaceHierarchyTest : InstrumentedTest {
}
return TestSpaceCreationResult(spaceId, roomIds)
}
@Test
fun testRootSpaces() {
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace(session, "SpaceA", listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
))
val spaceBInfo = createPublicSpace(session, "SpaceB", listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/),
Triple("B2", true, true),
Triple("B3", true, true)
))
val spaceCInfo = createPublicSpace(session, "SpaceC", listOf(
Triple("C1", true /*auto-join*/, true/*canonical*/),
Triple("C2", true, true)
))
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
// add C as subspace of B
runBlocking {
val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
}
// + A
// a1, a2
// + B
// b1, b2, b3
// + C
// + c1, c2
val rootSpaces = session.spaceService().getRootSpaceSummaries()
assertEquals(2, rootSpaces.size, "Unexpected number of root spaces ${rootSpaces.map { it.name }}")
}
}

View File

@ -90,4 +90,6 @@ interface SpaceService {
* if multiple are present the client should select the one with the lowest room ID, as determined via a lexicographic utf-8 ordering.
*/
suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: List<String>)
fun getRootSpaceSummaries(): List<RoomSummary>
}

View File

@ -123,6 +123,23 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
return getRoomSummaries(spaceSummaryQueryParams)
}
fun getRootSpaceSummaries(): List<RoomSummary> {
return getRoomSummaries(spaceSummaryQueryParams {
memberships = listOf(Membership.JOIN)
})
.let { allJoinedSpace ->
val allFlattenChildren = arrayListOf<RoomSummary>()
allJoinedSpace.forEach {
flattenSubSpace(it, emptyList(), allFlattenChildren, listOf(Membership.JOIN), false)
}
val knownNonOrphan = allFlattenChildren.map { it.roomId }.distinct()
// keep only root rooms
allJoinedSpace.filter { candidate ->
!knownNonOrphan.contains(candidate.roomId)
}
}
}
fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> {
return monarchy.fetchAllMappedSync(
{ breadcrumbsQuery(it, queryParams) },
@ -341,8 +358,14 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
}
}
fun flattenSubSpace(current: RoomSummary, parenting: List<String>, output: MutableList<RoomSummary>, memberShips: List<Membership>) {
output.add(current)
fun flattenSubSpace(current: RoomSummary,
parenting: List<String>,
output: MutableList<RoomSummary>,
memberShips: List<Membership>,
includeCurrent: Boolean = true) {
if (includeCurrent) {
output.add(current)
}
current.children?.sortedBy { it.order ?: it.name }?.forEach {
if (it.roomType == RoomType.SPACE) {
// Add recursive

View File

@ -85,6 +85,9 @@ internal class DefaultSpaceService @Inject constructor(
return roomSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams)
}
override fun getRootSpaceSummaries(): List<RoomSummary> {
return roomSummaryDataSource.getRootSpaceSummaries()
}
override suspend fun peekSpace(spaceId: String): SpacePeekResult {
return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId))
}

View File

@ -84,6 +84,10 @@ class SpaceListFragment @Inject constructor(
sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(spaceSummary.roomId))
}
override fun onToggleExpand(spaceSummary: RoomSummary) {
viewModel.handle(SpaceListAction.ToggleExpand(spaceSummary))
}
override fun onAddSpaceSelected() {
viewModel.handle(SpaceListAction.AddSpace)
}

View File

@ -27,6 +27,7 @@ import im.vector.app.features.grouplist.homeSpaceSummaryItem
import im.vector.app.features.home.AvatarRenderer
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.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
@ -48,10 +49,17 @@ class SpaceSummaryController @Inject constructor(
override fun buildModels() {
val nonNullViewState = viewState ?: return
buildGroupModels(nonNullViewState.asyncSpaces(), nonNullViewState.selectedSpace)
buildGroupModels(
nonNullViewState.asyncSpaces(),
nonNullViewState.selectedSpace,
nonNullViewState.rootSpaces,
nonNullViewState.expandedStates)
}
private fun buildGroupModels(summaries: List<RoomSummary>?, selected: RoomSummary?) {
private fun buildGroupModels(summaries: List<RoomSummary>?,
selected: RoomSummary?,
rootSpaces: List<RoomSummary>?,
expandedStates: Map<String, Boolean>) {
if (summaries.isNullOrEmpty()) {
return
}
@ -86,29 +94,66 @@ class SpaceSummaryController @Inject constructor(
text(stringProvider.getString(R.string.spaces_header))
}
summaries
.filter { it.membership == Membership.JOIN }
.forEach { groupSummary ->
val isSelected = groupSummary.roomId == selected?.roomId
if (groupSummary.roomId == ALL_COMMUNITIES_GROUP_ID) {
homeSpaceSummaryItem {
id(groupSummary.roomId)
selected(isSelected)
listener { callback?.onSpaceSelected(groupSummary) }
}
} else {
spaceSummaryItem {
avatarRenderer(avatarRenderer)
id(groupSummary.roomId)
matrixItem(groupSummary.toMatrixItem())
selected(isSelected)
onMore { callback?.onSpaceSettings(groupSummary) }
listener { callback?.onSpaceSelected(groupSummary) }
}
summaries.firstOrNull { it.roomId == ALL_COMMUNITIES_GROUP_ID }
?.let {
homeSpaceSummaryItem {
id(it.roomId)
selected(it.roomId == selected?.roomId)
listener { callback?.onSpaceSelected(it) }
}
}
// Temporary item to create a new Space (will move with final design)
// summaries
// .filter { it.membership == Membership.JOIN }
rootSpaces
?.forEach { groupSummary ->
val isSelected = groupSummary.roomId == selected?.roomId
// if (groupSummary.roomId == ALL_COMMUNITIES_GROUP_ID) {
// homeSpaceSummaryItem {
// id(groupSummary.roomId)
// selected(isSelected)
// listener { callback?.onSpaceSelected(groupSummary) }
// }
// } else {
// does it have children?
val subSpaces = groupSummary.children?.filter { childInfo ->
summaries.indexOfFirst { it.roomId == childInfo.childRoomId } != -1
}
val hasChildren = (subSpaces?.size ?: 0) > 0
val expanded = expandedStates[groupSummary.roomId] == true
spaceSummaryItem {
avatarRenderer(avatarRenderer)
id(groupSummary.roomId)
hasChildren(hasChildren)
expanded(expanded)
matrixItem(groupSummary.toMatrixItem())
selected(isSelected)
onMore { callback?.onSpaceSettings(groupSummary) }
listener { callback?.onSpaceSelected(groupSummary) }
toggleExpand { callback?.onToggleExpand(groupSummary) }
}
if (hasChildren && expanded) {
// it's expanded
subSpaces?.forEach { child ->
summaries.firstOrNull { it.roomId == child.childRoomId }?.let { childSum ->
val isSelected = childSum.roomId == selected?.roomId
spaceSummaryItem {
avatarRenderer(avatarRenderer)
id(child.childRoomId)
hasChildren(false)
selected(isSelected)
matrixItem(MatrixItem.RoomItem(child.childRoomId, child.name, child.avatarUrl))
listener { callback?.onSpaceSelected(childSum) }
indent(1)
}
}
}
}
}
// Temporary item to create a new Space (will move with final design)
genericButtonItem {
id("create")
@ -121,6 +166,7 @@ class SpaceSummaryController @Inject constructor(
interface Callback {
fun onSpaceSelected(spaceSummary: RoomSummary)
fun onSpaceSettings(spaceSummary: RoomSummary)
fun onToggleExpand(spaceSummary: RoomSummary)
fun onAddSpaceSelected()
}
}

View File

@ -17,6 +17,7 @@
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.isGone
@ -40,7 +41,9 @@ abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
@EpoxyAttribute var listener: (() -> Unit)? = null
@EpoxyAttribute var onMore: (() -> Unit)? = null
@EpoxyAttribute var toggleExpand: (() -> Unit)? = null
@EpoxyAttribute var expanded: Boolean? = null
@EpoxyAttribute var expanded: Boolean = false
@EpoxyAttribute var hasChildren: Boolean = false
@EpoxyAttribute var indent: Int = 0
override fun bind(holder: Holder) {
super.bind(holder)
@ -57,25 +60,36 @@ abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
holder.moreView.isVisible = false
}
when (expanded) {
null -> {
holder.collapseIndicator.isGone = true
}
else -> {
holder.collapseIndicator.isVisible = true
holder.collapseIndicator.setImageDrawable(
ContextCompat.getDrawable(holder.view.context,
if (expanded!!) R.drawable.ic_expand_less else R.drawable.ic_expand_more
)
)
holder.collapseIndicator.setOnClickListener(
DebouncedClickListener({ _ ->
toggleExpand?.invoke()
})
)
}
if (hasChildren) {
// holder.collapseIndicator.setOnClickListener(
// DebouncedClickListener({ _ ->
// toggleExpand?.invoke()
// })
// )
// when (expanded) {
// null -> {
// holder.collapseIndicator.isGone = true
// }
// else -> {
holder.collapseIndicator.isVisible = true
holder.collapseIndicator.setImageDrawable(
ContextCompat.getDrawable(holder.view.context,
if (expanded) R.drawable.ic_expand_less else R.drawable.ic_expand_more
)
)
holder.collapseIndicator.setOnClickListener(
DebouncedClickListener({ _ ->
toggleExpand?.invoke()
})
)
// }
// }
} else {
holder.collapseIndicator.isGone = true
}
holder.indentSpace.isVisible = indent > 0
avatarRenderer.renderSpace(matrixItem, holder.avatarImageView)
}
@ -90,5 +104,6 @@ abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
val rootView by bind<CheckableConstraintLayout>(R.id.itemGroupLayout)
val moreView by bind<ImageView>(R.id.groupTmpLeave)
val collapseIndicator by bind<ImageView>(R.id.groupChildrenCollapse)
val indentSpace by bind<Space>(R.id.indent)
}
}

View File

@ -51,6 +51,7 @@ const val ALL_COMMUNITIES_GROUP_ID = "+ALL_COMMUNITIES_GROUP_ID"
sealed class SpaceListAction : VectorViewModelAction {
data class SelectSpace(val spaceSummary: RoomSummary) : SpaceListAction()
data class LeaveSpace(val spaceSummary: RoomSummary) : SpaceListAction()
data class ToggleExpand(val spaceSummary: RoomSummary) : SpaceListAction()
object AddSpace : SpaceListAction()
}
@ -65,7 +66,9 @@ sealed class SpaceListViewEvents : VectorViewEvents {
data class SpaceListViewState(
val asyncSpaces: Async<List<RoomSummary>> = Uninitialized,
val selectedSpace: RoomSummary? = null
val selectedSpace: RoomSummary? = null,
val rootSpaces: List<RoomSummary>? = null,
val expandedStates: Map<String, Boolean> = emptyMap()
) : MvRxState
class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: SpaceListViewState,
@ -132,6 +135,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
is SpaceListAction.SelectSpace -> handleSelectSpace(action)
is SpaceListAction.LeaveSpace -> handleLeaveSpace(action)
SpaceListAction.AddSpace -> handleAddSpace()
is SpaceListAction.ToggleExpand -> handleToggleExpand(action)
}
}
@ -159,6 +163,15 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
}
}
private fun handleToggleExpand(action: SpaceListAction.ToggleExpand) = withState { state ->
val updatedToggleStates = state.expandedStates.toMutableMap().apply {
this[action.spaceSummary.roomId] = !(this[action.spaceSummary.roomId] ?: false)
}
setState {
copy(expandedStates = updatedToggleStates)
}
}
private fun handleLeaveSpace(action: SpaceListAction.LeaveSpace) {
viewModelScope.launch {
awaitCallback {
@ -199,7 +212,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
.rx()
.liveSpaceSummaries(spaceSummaryQueryParams),
BiFunction { allCommunityGroup, communityGroups ->
listOf(allCommunityGroup) + communityGroups
(listOf(allCommunityGroup) + communityGroups)
}
)
.execute { async ->
@ -209,7 +222,11 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
} else {
async()?.firstOrNull()
}
copy(asyncSpaces = async, selectedSpace = newSelectedGroup)
copy(
asyncSpaces = async,
selectedSpace = newSelectedGroup,
rootSpaces = session.spaceService().getRootSpaceSummaries()
)
}
}
}

View File

@ -10,6 +10,16 @@
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<Space
android:id="@+id/indent"
android:layout_width="30dp"
android:layout_height="match_parent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ImageView
android:id="@+id/groupAvatarImageView"
android:layout_width="42dp"
@ -19,7 +29,7 @@
android:contentDescription="@string/avatar"
android:duplicateParentState="true"
app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="@id/indent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
@ -46,31 +56,33 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="?selectableItemBackground"
android:clickable="true"
android:importantForAccessibility="no"
tools:src="@drawable/ic_expand_more_white"
android:src="@drawable/ic_expand_less_white"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator"
app:layout_constraintEnd_toStartOf="@+id/groupTmpLeave"
app:layout_constraintTop_toTopOf="parent"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix" />
tools:ignore="MissingPrefix"
tools:src="@drawable/ic_expand_more_white"
tools:visibility="visible" />
<ImageView
android:id="@+id/groupTmpLeave"
android:clickable="true"
android:background="?selectableItemBackground"
android:layout_width="30dp"
android:layout_height="30dp"
android:padding="4dp"
android:layout_marginEnd="4dp"
android:background="?selectableItemBackground"
android:clickable="true"
android:importantForAccessibility="no"
android:padding="4dp"
android:src="@drawable/ic_more_vertical"
app:tint="?riotx_text_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/groupAvatarChevron"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
app:tint="?riotx_text_secondary" />
<ImageView
android:id="@+id/groupAvatarChevron"
@ -78,8 +90,8 @@
android:layout_height="wrap_content"
android:layout_marginEnd="21dp"
android:importantForAccessibility="no"
android:visibility="gone"
android:src="@drawable/ic_arrow_right"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"