1 depth hierarchy support in space panel
This commit is contained in:
parent
872b383c4d
commit
ea5e48b940
|
@ -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 }}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue