Support Space explore pagination

This commit is contained in:
Valere 2021-08-31 11:50:23 +02:00
parent 2982fdc626
commit 5297512f87
30 changed files with 463 additions and 264 deletions

1
changelog.d/3693.feature Normal file
View File

@ -0,0 +1 @@
Space summary pagination

View File

@ -27,7 +27,7 @@ data class SpaceChildInfo(
val avatarUrl: String?, val avatarUrl: String?,
val order: String?, val order: String?,
val activeMemberCount: Int?, val activeMemberCount: Int?,
val autoJoin: Boolean, // val autoJoin: Boolean,
val viaServers: List<String>, val viaServers: List<String>,
val parentRoomId: String?, val parentRoomId: String?,
val suggested: Boolean?, val suggested: Boolean?,

View File

@ -36,7 +36,7 @@ interface Space {
suspend fun addChildren(roomId: String, suspend fun addChildren(roomId: String,
viaServers: List<String>?, viaServers: List<String>?,
order: String?, order: String?,
autoJoin: Boolean = false, // autoJoin: Boolean = false,
suggested: Boolean? = false) suggested: Boolean? = false)
fun getChildInfo(roomId: String): SpaceChildContent? fun getChildInfo(roomId: String): SpaceChildContent?
@ -46,8 +46,8 @@ interface Space {
@Throws @Throws
suspend fun setChildrenOrder(roomId: String, order: String?) suspend fun setChildrenOrder(roomId: String, order: String?)
@Throws // @Throws
suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean) // suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean)
@Throws @Throws
suspend fun setChildrenSuggested(roomId: String, suggested: Boolean) suspend fun setChildrenSuggested(roomId: String, suggested: Boolean)

View File

@ -18,9 +18,10 @@ package org.matrix.android.sdk.api.session.space
import android.net.Uri import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.model.RoomSummary 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.internal.session.space.SpaceHierarchySummary
import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
typealias SpaceSummaryQueryParams = RoomSummaryQueryParams typealias SpaceSummaryQueryParams = RoomSummaryQueryParams
@ -58,10 +59,18 @@ interface SpaceService {
/** /**
* Get's information of a space by querying the server * Get's information of a space by querying the server
* @param suggestedOnly If true, return only child events and rooms where the m.space.child event has suggested: true.
* @param limit a client-defined limit to the maximum number of rooms to return per page. Must be a non-negative integer.
* @param maxDepth: Optional: The maximum depth in the tree (from the root room) to return.
* @param from: Optional. Pagination token given to retrieve the next set of rooms. Note that if a pagination token is provided,
* then the parameters given for suggested_only and max_depth must be the same.
*/ */
suspend fun querySpaceChildren(spaceId: String, suspend fun querySpaceChildren(spaceId: String,
suggestedOnly: Boolean? = null, suggestedOnly: Boolean? = null,
autoJoinedOnly: Boolean? = null): Pair<RoomSummary, List<SpaceChildInfo>> limit: Int? = null,
from: String? = null,
// when paginating, pass back the m.space.child state events
knownStateList: List<Event>? = null): SpaceHierarchySummary
/** /**
* Get a live list of space summaries. This list is refreshed as soon as the data changes. * Get a live list of space summaries. This list is refreshed as soon as the data changes.

View File

@ -40,12 +40,12 @@ data class SpaceChildContent(
* or consist of more than 50 characters, are forbidden and should be ignored if received.) * or consist of more than 50 characters, are forbidden and should be ignored if received.)
*/ */
@Json(name = "order") val order: String? = null, @Json(name = "order") val order: String? = null,
/** // /**
* The auto_join flag on a child listing allows a space admin to list the sub-spaces and rooms in that space which should // * The auto_join flag on a child listing allows a space admin to list the sub-spaces and rooms in that space which should
* be automatically joined by members of that space. // * be automatically joined by members of that space.
* (This is not a force-join, which are descoped for a future MSC; the user can subsequently part these room if they desire.) // * (This is not a force-join, which are descoped for a future MSC; the user can subsequently part these room if they desire.)
*/ // */
@Json(name = "auto_join") val autoJoin: Boolean? = false, // @Json(name = "auto_join") val autoJoin: Boolean? = false,
/** /**
* If `suggested` is set to `true`, that indicates that the child should be advertised to * If `suggested` is set to `true`, that indicates that the child should be advertised to

View File

@ -88,7 +88,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
avatarUrl = it.childSummaryEntity?.avatarUrl, avatarUrl = it.childSummaryEntity?.avatarUrl,
activeMemberCount = it.childSummaryEntity?.joinedMembersCount, activeMemberCount = it.childSummaryEntity?.joinedMembersCount,
order = it.order, order = it.order,
autoJoin = it.autoJoin ?: false, // autoJoin = it.autoJoin ?: false,
viaServers = it.viaServers.toList(), viaServers = it.viaServers.toList(),
parentRoomId = roomSummaryEntity.roomId, parentRoomId = roomSummaryEntity.roomId,
suggested = it.suggested, suggested = it.suggested,

View File

@ -43,7 +43,7 @@ internal class RoomChildRelationInfo(
data class SpaceChildInfo( data class SpaceChildInfo(
val roomId: String, val roomId: String,
val order: String?, val order: String?,
val autoJoin: Boolean, // val autoJoin: Boolean,
val viaServers: List<String> val viaServers: List<String>
) )
@ -71,7 +71,7 @@ internal class RoomChildRelationInfo(
SpaceChildInfo( SpaceChildInfo(
roomId = it.stateKey, roomId = it.stateKey,
order = scc.validOrder(), order = scc.validOrder(),
autoJoin = scc.autoJoin ?: false, // autoJoin = scc.autoJoin ?: false,
viaServers = via viaServers = via
) )
} }

View File

@ -220,7 +220,7 @@ internal class RoomSummaryUpdater @Inject constructor(
this.childRoomId = child.roomId this.childRoomId = child.roomId
this.childSummaryEntity = RoomSummaryEntity.where(realm, child.roomId).findFirst() this.childSummaryEntity = RoomSummaryEntity.where(realm, child.roomId).findFirst()
this.order = child.order this.order = child.order
this.autoJoin = child.autoJoin // this.autoJoin = child.autoJoin
this.viaServers.addAll(child.viaServers) this.viaServers.addAll(child.viaServers)
} }
) )

View File

@ -51,7 +51,7 @@ internal class DefaultSpace(
override suspend fun addChildren(roomId: String, override suspend fun addChildren(roomId: String,
viaServers: List<String>?, viaServers: List<String>?,
order: String?, order: String?,
autoJoin: Boolean, // autoJoin: Boolean,
suggested: Boolean?) { suggested: Boolean?) {
// Find best via // Find best via
val bestVia = viaServers val bestVia = viaServers
@ -69,7 +69,6 @@ internal class DefaultSpace(
stateKey = roomId, stateKey = roomId,
body = SpaceChildContent( body = SpaceChildContent(
via = bestVia, via = bestVia,
autoJoin = autoJoin,
order = order, order = order,
suggested = suggested suggested = suggested
).toContent() ).toContent()
@ -90,7 +89,7 @@ internal class DefaultSpace(
body = SpaceChildContent( body = SpaceChildContent(
order = null, order = null,
via = null, via = null,
autoJoin = null, // autoJoin = null,
suggested = null suggested = null
).toContent() ).toContent()
) )
@ -115,35 +114,35 @@ internal class DefaultSpace(
body = SpaceChildContent( body = SpaceChildContent(
order = order, order = order,
via = existing.via, via = existing.via,
autoJoin = existing.autoJoin, // autoJoin = existing.autoJoin,
suggested = existing.suggested suggested = existing.suggested
).toContent() ).toContent()
) )
} }
override suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean) { // override suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean) {
val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) // val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
.firstOrNull() // .firstOrNull()
?.content.toModel<SpaceChildContent>() // ?.content.toModel<SpaceChildContent>()
?: throw IllegalArgumentException("$roomId is not a child of this space") // ?: throw IllegalArgumentException("$roomId is not a child of this space")
//
if (existing.autoJoin == autoJoin) { // if (existing.autoJoin == autoJoin) {
// nothing to do? // // nothing to do?
return // return
} // }
//
// edit state event and set via to null // // edit state event and set via to null
room.sendStateEvent( // room.sendStateEvent(
eventType = EventType.STATE_SPACE_CHILD, // eventType = EventType.STATE_SPACE_CHILD,
stateKey = roomId, // stateKey = roomId,
body = SpaceChildContent( // body = SpaceChildContent(
order = existing.order, // order = existing.order,
via = existing.via, // via = existing.via,
autoJoin = autoJoin, // autoJoin = autoJoin,
suggested = existing.suggested // suggested = existing.suggested
).toContent() // ).toContent()
) // )
} // }
override suspend fun setChildrenSuggested(roomId: String, suggested: Boolean) { override suspend fun setChildrenSuggested(roomId: String, suggested: Boolean) {
val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
@ -162,7 +161,7 @@ internal class DefaultSpace(
body = SpaceChildContent( body = SpaceChildContent(
order = existing.order, order = existing.order,
via = existing.via, via = existing.via,
autoJoin = existing.autoJoin, // autoJoin = existing.autoJoin,
suggested = suggested suggested = suggested
).toContent() ).toContent()
) )

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.space
import android.net.Uri import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -27,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomSummary 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.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
@ -108,53 +110,65 @@ internal class DefaultSpaceService @Inject constructor(
override suspend fun querySpaceChildren(spaceId: String, override suspend fun querySpaceChildren(spaceId: String,
suggestedOnly: Boolean?, suggestedOnly: Boolean?,
autoJoinedOnly: Boolean?): Pair<RoomSummary, List<SpaceChildInfo>> { limit: Int?,
return resolveSpaceInfoTask.execute(ResolveSpaceInfoTask.Params.withId(spaceId, suggestedOnly, autoJoinedOnly)).let { response -> from: String?,
knownStateList: List<Event>?): SpaceHierarchySummary {
return resolveSpaceInfoTask.execute(
ResolveSpaceInfoTask.Params(
spaceId = spaceId, limit = limit, maxDepth = 1, from = from, suggestedOnly = suggestedOnly
)
).let { response ->
val spaceDesc = response.rooms?.firstOrNull { it.roomId == spaceId } val spaceDesc = response.rooms?.firstOrNull { it.roomId == spaceId }
Pair( val root = RoomSummary(
first = RoomSummary( roomId = spaceDesc?.roomId ?: spaceId,
roomId = spaceDesc?.roomId ?: spaceId, roomType = spaceDesc?.roomType,
roomType = spaceDesc?.roomType, name = spaceDesc?.name ?: "",
name = spaceDesc?.name ?: "", displayName = spaceDesc?.name ?: "",
displayName = spaceDesc?.name ?: "", topic = spaceDesc?.topic ?: "",
topic = spaceDesc?.topic ?: "", joinedMembersCount = spaceDesc?.numJoinedMembers,
joinedMembersCount = spaceDesc?.numJoinedMembers, avatarUrl = spaceDesc?.avatarUrl ?: "",
avatarUrl = spaceDesc?.avatarUrl ?: "", encryptionEventTs = null,
encryptionEventTs = null, typingUsers = emptyList(),
typingUsers = emptyList(), isEncrypted = false,
isEncrypted = false, flattenParentIds = emptyList(),
flattenParentIds = emptyList() canonicalAlias = spaceDesc?.canonicalAlias,
), joinRules = RoomJoinRules.PUBLIC.takeIf { spaceDesc?.worldReadable == true }
second = response.rooms )
?.filter { it.roomId != spaceId } val children = response.rooms
?.flatMap { childSummary -> ?.filter { it.roomId != spaceId }
response.events ?.flatMap { childSummary ->
?.filter { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD } (spaceDesc?.childrenState ?: knownStateList)
?.mapNotNull { childStateEv -> ?.filter { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD }
// create a child entry for everytime this room is the child of a space ?.mapNotNull { childStateEv ->
// beware that a room could appear then twice in this list // create a child entry for everytime this room is the child of a space
childStateEv.content.toModel<SpaceChildContent>()?.let { childStateEvContent -> // beware that a room could appear then twice in this list
SpaceChildInfo( childStateEv.content.toModel<SpaceChildContent>()?.let { childStateEvContent ->
childRoomId = childSummary.roomId, SpaceChildInfo(
isKnown = true, childRoomId = childSummary.roomId,
roomType = childSummary.roomType, isKnown = true,
name = childSummary.name, roomType = childSummary.roomType,
topic = childSummary.topic, name = childSummary.name,
avatarUrl = childSummary.avatarUrl, topic = childSummary.topic,
order = childStateEvContent.order, avatarUrl = childSummary.avatarUrl,
autoJoin = childStateEvContent.autoJoin ?: false, order = childStateEvContent.order,
viaServers = childStateEvContent.via.orEmpty(), // autoJoin = childStateEvContent.autoJoin ?: false,
activeMemberCount = childSummary.numJoinedMembers, viaServers = childStateEvContent.via.orEmpty(),
parentRoomId = childStateEv.roomId, activeMemberCount = childSummary.numJoinedMembers,
suggested = childStateEvContent.suggested, parentRoomId = childStateEv.roomId,
canonicalAlias = childSummary.canonicalAlias, suggested = childStateEvContent.suggested,
aliases = childSummary.aliases, canonicalAlias = childSummary.canonicalAlias,
worldReadable = childSummary.worldReadable aliases = childSummary.aliases,
) worldReadable = childSummary.worldReadable
} )
}.orEmpty() }
} }.orEmpty()
.orEmpty() }
.orEmpty()
SpaceHierarchySummary(
rootSummary = root,
children = children,
childrenState = spaceDesc?.childrenState.orEmpty(),
nextToken = response.nextBatch
) )
} }
} }

View File

@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.space
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.space.JoinSpaceResult import org.matrix.android.sdk.api.session.space.JoinSpaceResult
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
@ -84,39 +83,39 @@ internal class DefaultJoinSpaceTask @Inject constructor(
// after that i should have the children (? do I need to paginate to get state) // after that i should have the children (? do I need to paginate to get state)
val summary = roomSummaryDataSource.getSpaceSummary(params.roomIdOrAlias) val summary = roomSummaryDataSource.getSpaceSummary(params.roomIdOrAlias)
Timber.v("## Space: Found space summary Name:[${summary?.name}] children: ${summary?.spaceChildren?.size}") Timber.v("## Space: Found space summary Name:[${summary?.name}] children: ${summary?.spaceChildren?.size}")
summary?.spaceChildren?.forEach { // summary?.spaceChildren?.forEach {
// val childRoomSummary = it.roomSummary ?: return@forEach // val childRoomSummary = it.roomSummary ?: return@forEach
Timber.v("## Space: Processing child :[${it.childRoomId}] autoJoin:${it.autoJoin}") // Timber.v("## Space: Processing child :[${it.childRoomId}] suggested:${it.suggested}")
if (it.autoJoin) { // if (it.autoJoin) {
// I should try to join as well // // I should try to join as well
if (it.roomType == RoomType.SPACE) { // if (it.roomType == RoomType.SPACE) {
// recursively join auto-joined child of this space? // // recursively join auto-joined child of this space?
when (val subspaceJoinResult = execute(JoinSpaceTask.Params(it.childRoomId, null, it.viaServers))) { // when (val subspaceJoinResult = execute(JoinSpaceTask.Params(it.childRoomId, null, it.viaServers))) {
JoinSpaceResult.Success -> { // JoinSpaceResult.Success -> {
// nop // // nop
} // }
is JoinSpaceResult.Fail -> { // is JoinSpaceResult.Fail -> {
errors[it.childRoomId] = subspaceJoinResult.error // errors[it.childRoomId] = subspaceJoinResult.error
} // }
is JoinSpaceResult.PartialSuccess -> { // is JoinSpaceResult.PartialSuccess -> {
errors.putAll(subspaceJoinResult.failedRooms) // errors.putAll(subspaceJoinResult.failedRooms)
} // }
} // }
} else { // } else {
try { // try {
Timber.v("## Space: Joining room child ${it.childRoomId}") // Timber.v("## Space: Joining room child ${it.childRoomId}")
joinRoomTask.execute(JoinRoomTask.Params( // joinRoomTask.execute(JoinRoomTask.Params(
roomIdOrAlias = it.childRoomId, // roomIdOrAlias = it.childRoomId,
reason = "Auto-join parent space", // reason = "Auto-join parent space",
viaServers = it.viaServers // viaServers = it.viaServers
)) // ))
} catch (failure: Throwable) { // } catch (failure: Throwable) {
errors[it.childRoomId] = failure // errors[it.childRoomId] = failure
Timber.e("## Space: Failed to join room child ${it.childRoomId}") // Timber.e("## Space: Failed to join room child ${it.childRoomId}")
} // }
} // }
} // }
} // }
return if (errors.isEmpty()) { return if (errors.isEmpty()) {
JoinSpaceResult.Success JoinSpaceResult.Success

View File

@ -24,23 +24,24 @@ import javax.inject.Inject
internal interface ResolveSpaceInfoTask : Task<ResolveSpaceInfoTask.Params, SpacesResponse> { internal interface ResolveSpaceInfoTask : Task<ResolveSpaceInfoTask.Params, SpacesResponse> {
data class Params( data class Params(
val spaceId: String, val spaceId: String,
val maxRoomPerSpace: Int?, // val maxRoomPerSpace: Int?,
val limit: Int, val limit: Int?,
val batchToken: String?, val maxDepth: Int?,
val suggestedOnly: Boolean?, val from: String?,
val autoJoinOnly: Boolean? val suggestedOnly: Boolean?
// val autoJoinOnly: Boolean?
) { ) {
companion object { // companion object {
fun withId(spaceId: String, suggestedOnly: Boolean?, autoJoinOnly: Boolean?) = // fun withId(spaceId: String, suggestedOnly: Boolean?) =
Params( // Params(
spaceId = spaceId, // spaceId = spaceId,
maxRoomPerSpace = 10, // // maxRoomPerSpace = 10,
limit = 20, // limit = 20,
batchToken = null, // from = null,
suggestedOnly = suggestedOnly, // suggestedOnly = suggestedOnly
autoJoinOnly = autoJoinOnly // // autoJoinOnly = autoJoinOnly
) // )
} // }
} }
} }
@ -49,15 +50,13 @@ internal class DefaultResolveSpaceInfoTask @Inject constructor(
private val globalErrorReceiver: GlobalErrorReceiver private val globalErrorReceiver: GlobalErrorReceiver
) : ResolveSpaceInfoTask { ) : ResolveSpaceInfoTask {
override suspend fun execute(params: ResolveSpaceInfoTask.Params): SpacesResponse { override suspend fun execute(params: ResolveSpaceInfoTask.Params): SpacesResponse {
val body = SpaceSummaryParams(
maxRoomPerSpace = params.maxRoomPerSpace,
limit = params.limit,
batch = params.batchToken ?: "",
autoJoinedOnly = params.autoJoinOnly,
suggestedOnly = params.suggestedOnly
)
return executeRequest(globalErrorReceiver) { return executeRequest(globalErrorReceiver) {
spaceApi.getSpaces(params.spaceId, body) spaceApi.getSpaceHierarchy(
spaceId = params.spaceId,
suggestedOnly = params.suggestedOnly,
limit = params.limit,
maxDepth = params.maxDepth,
from = params.from)
} }
} }
} }

View File

@ -17,9 +17,9 @@
package org.matrix.android.sdk.internal.session.space package org.matrix.android.sdk.internal.session.space
import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.network.NetworkConstants
import retrofit2.http.Body import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query
internal interface SpaceApi { internal interface SpaceApi {
@ -37,7 +37,23 @@ internal interface SpaceApi {
* - MSC 2946 https://github.com/matrix-org/matrix-doc/blob/kegan/spaces-summary/proposals/2946-spaces-summary.md * - MSC 2946 https://github.com/matrix-org/matrix-doc/blob/kegan/spaces-summary/proposals/2946-spaces-summary.md
* - https://hackmd.io/fNYh4tjUT5mQfR1uuRzWDA * - https://hackmd.io/fNYh4tjUT5mQfR1uuRzWDA
*/ */
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/spaces") // @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/spaces")
suspend fun getSpaces(@Path("roomId") spaceId: String, // suspend fun getSpaces(@Path("roomId") spaceId: String,
@Body params: SpaceSummaryParams): SpacesResponse // @Body params: SpaceSummaryParams): SpacesResponse
/**
* @param limit: Optional: a client-defined limit to the maximum number of rooms to return per page. Must be a non-negative integer.
* @param max_depth: Optional: The maximum depth in the tree (from the root room) to return.
* The deepest depth returned will not include children events. Defaults to no-limit. Must be a non-negative integer.
*
* @param from: Optional. Pagination token given to retrieve the next set of rooms.
* Note that if a pagination token is provided, then the parameters given for suggested_only and max_depth must be the same.
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/hierarchy")
suspend fun getSpaceHierarchy(
@Path("roomId") spaceId: String,
@Query("suggested_only") suggestedOnly: Boolean?,
@Query("limit") limit: Int?,
@Query("max_depth") maxDepth: Int?,
@Query("from") from: String?): SpacesResponse
} }

View File

@ -18,14 +18,21 @@ package org.matrix.android.sdk.internal.session.space
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Event
/**
* The fields are the same as those returned by /publicRooms (see spec), with the addition of:
* room_type: the value of the m.type field from the room's m.room.create event, if any.
* children_state: The m.space.child events of the room. For each event, only the following fields are included1: type, state_key, content, room_id, sender, with the addition of:
* origin_server_ts: This is required for sorting of rooms as specified below.
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class SpaceChildSummaryResponse( internal data class SpaceChildSummaryResponse(
/** // /**
* The total number of state events which point to or from this room (inbound/outbound edges). // * The total number of state events which point to or from this room (inbound/outbound edges).
* This includes all m.space.child events in the room, in addition to m.room.parent events which point to this room as a parent. // * This includes all m.space.child events in the room, in addition to m.room.parent events which point to this room as a parent.
*/ // */
@Json(name = "num_refs") val numRefs: Int? = null, // @Json(name = "num_refs") val numRefs: Int? = null,
/** /**
* The room type, which is m.space for subspaces. * The room type, which is m.space for subspaces.
@ -33,6 +40,11 @@ internal data class SpaceChildSummaryResponse(
*/ */
@Json(name = "room_type") val roomType: String? = null, @Json(name = "room_type") val roomType: String? = null,
/** The m.space.child events of the room. For each event, only the following fields are included:
* type, state_key, content, room_id, sender, with the addition of origin_server_ts: This is required for sorting of rooms as specified below.
*/
@Json(name = "children_state") val childrenState: List<Event>? = null,
/** /**
* Aliases of the room. May be empty. * Aliases of the room. May be empty.
*/ */

View File

@ -0,0 +1,28 @@
/*
* 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 org.matrix.android.sdk.internal.session.space
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
data class SpaceHierarchySummary(
val rootSummary: RoomSummary,
val children: List<SpaceChildInfo>,
val childrenState: List<Event>,
val nextToken: String? = null
)

View File

@ -1,34 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.space
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class SpaceSummaryParams(
/** The maximum number of rooms/subspaces to return for a given space, if negative unbounded. default: -1 */
@Json(name = "max_rooms_per_space") val maxRoomPerSpace: Int?,
/** The maximum number of rooms/subspaces to return, server can override this, default: 100 */
@Json(name = "limit") val limit: Int?,
/** A token to use if this is a subsequent HTTP hit, default: "". */
@Json(name = "batch") val batch: String = "",
/** whether we should only return children with the "suggested" flag set. */
@Json(name = "suggested_only") val suggestedOnly: Boolean?,
/** whether we should only return children with the "suggested" flag set. */
@Json(name = "auto_join_only") val autoJoinedOnly: Boolean?
)

View File

@ -18,14 +18,11 @@ package org.matrix.android.sdk.internal.session.space
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Event
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class SpacesResponse( internal data class SpacesResponse(
/** Its presence indicates that there are more results to return. */ /** Its presence indicates that there are more results to return. */
@Json(name = "next_batch") val nextBatch: String? = null, @Json(name = "next_batch") val nextBatch: String? = null,
/** Rooms information like name/avatar/type ... */ /** Rooms information like name/avatar/type ... */
@Json(name = "rooms") val rooms: List<SpaceChildSummaryResponse>? = null, @Json(name = "rooms") val rooms: List<SpaceChildSummaryResponse>? = null
/** These are the edges of the graph. The objects in the array are complete (or stripped?) m.room.parent or m.space.child events. */
@Json(name = "events") val events: List<Event>? = null
) )

View File

@ -103,7 +103,7 @@ internal class DefaultPeekSpaceTask @Inject constructor(
// can't peek :/ // can't peek :/
spaceChildResults.add( spaceChildResults.add(
SpaceChildPeekResult( SpaceChildPeekResult(
childId, childPeek, entry.second?.autoJoin, entry.second?.order childId, childPeek, entry.second?.order
) )
) )
// continue to next child // continue to next child
@ -116,7 +116,7 @@ internal class DefaultPeekSpaceTask @Inject constructor(
SpaceSubChildPeekResult( SpaceSubChildPeekResult(
childId, childId,
childPeek, childPeek,
entry.second?.autoJoin, // entry.second?.autoJoin,
entry.second?.order, entry.second?.order,
peekChildren(childStateEvents, depth + 1, maxDepth) peekChildren(childStateEvents, depth + 1, maxDepth)
) )
@ -127,7 +127,7 @@ internal class DefaultPeekSpaceTask @Inject constructor(
Timber.v("## SPACE_PEEK: room child $entry") Timber.v("## SPACE_PEEK: room child $entry")
spaceChildResults.add( spaceChildResults.add(
SpaceChildPeekResult( SpaceChildPeekResult(
childId, childPeek, entry.second?.autoJoin, entry.second?.order childId, childPeek, entry.second?.order
) )
) )
} }

View File

@ -28,21 +28,21 @@ data class SpacePeekSummary(
interface ISpaceChild { interface ISpaceChild {
val id: String val id: String
val roomPeekResult: PeekResult val roomPeekResult: PeekResult
val default: Boolean? // val default: Boolean?
val order: String? val order: String?
} }
data class SpaceChildPeekResult( data class SpaceChildPeekResult(
override val id: String, override val id: String,
override val roomPeekResult: PeekResult, override val roomPeekResult: PeekResult,
override val default: Boolean? = null, // override val default: Boolean? = null,
override val order: String? = null override val order: String? = null
) : ISpaceChild ) : ISpaceChild
data class SpaceSubChildPeekResult( data class SpaceSubChildPeekResult(
override val id: String, override val id: String,
override val roomPeekResult: PeekResult, override val roomPeekResult: PeekResult,
override val default: Boolean?, // override val default: Boolean?,
override val order: String?, override val order: String?,
val children: List<ISpaceChild> val children: List<ISpaceChild>
) : ISpaceChild ) : ISpaceChild

View File

@ -80,7 +80,7 @@ class UpgradeRoomViewModelTask @Inject constructor(
roomId = updatedRoomId, roomId = updatedRoomId,
viaServers = currentInfo.via, viaServers = currentInfo.via,
order = currentInfo.order, order = currentInfo.order,
autoJoin = currentInfo.autoJoin ?: false, // autoJoin = currentInfo.autoJoin ?: false,
suggested = currentInfo.suggested suggested = currentInfo.suggested
) )

View File

@ -230,8 +230,11 @@ class RoomListSectionBuilderSpace(
Observable.just(emptyList()) Observable.just(emptyList())
} else { } else {
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) { liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
val spaceSum = tryOrNull { session.spaceService().querySpaceChildren(selectedSpace.roomId, suggestedOnly = true) } val spaceSum = tryOrNull {
val value = spaceSum?.second.orEmpty().distinctBy { it.childRoomId } session.spaceService()
.querySpaceChildren(selectedSpace.roomId, suggestedOnly = true, null, null)
}
val value = spaceSum?.children.orEmpty().distinctBy { it.childRoomId }
// i need to check if it's already joined. // i need to check if it's already joined.
val filtered = value.filter { val filtered = value.filter {
session.getRoomSummary(it.childRoomId)?.membership?.isActive() != true session.getRoomSummary(it.childRoomId)?.membership?.isActive() != true

View File

@ -62,7 +62,7 @@ class SpaceCardRenderer @Inject constructor(
inCard.matrixToAccessImage.isVisible = true inCard.matrixToAccessImage.isVisible = true
inCard.matrixToAccessImage.setImageResource(R.drawable.ic_room_private) inCard.matrixToAccessImage.setImageResource(R.drawable.ic_room_private)
} }
val memberCount = spaceSummary.otherMemberIds.size val memberCount = spaceSummary.joinedMembersCount?.let { it - 1 } ?: 0
if (memberCount != 0) { if (memberCount != 0) {
inCard.matrixToMemberPills.isVisible = true inCard.matrixToMemberPills.isVisible = true
inCard.spaceChildMemberCountText.text = stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount) inCard.spaceChildMemberCountText.text = stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)

View File

@ -134,7 +134,7 @@ class CreateSpaceViewModelTask @Inject constructor(
timeout.roomID timeout.roomID
} }
val via = session.sessionParams.homeServerHost?.let { listOf(it) } ?: emptyList() val via = session.sessionParams.homeServerHost?.let { listOf(it) } ?: emptyList()
createdSpace!!.addChildren(roomId, via, null, autoJoin = false, suggested = true) createdSpace!!.addChildren(roomId, via, null, suggested = true)
// set canonical // set canonical
session.spaceService().setSpaceParent( session.spaceService().setSpaceParent(
roomId, roomId,

View File

@ -18,8 +18,10 @@ package im.vector.app.features.spaces.explore
import android.view.View import android.view.View
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.epoxy.VisibilityState
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.errorWithRetryItem
@ -54,13 +56,15 @@ class SpaceDirectoryController @Inject constructor(
fun onRoomClick(spaceChildInfo: SpaceChildInfo) fun onRoomClick(spaceChildInfo: SpaceChildInfo)
fun retry() fun retry()
fun addExistingRooms(spaceId: String) fun addExistingRooms(spaceId: String)
fun loadAdditionalItemsIfNeeded()
} }
var listener: InteractionListener? = null var listener: InteractionListener? = null
override fun buildModels(data: SpaceDirectoryState?) { override fun buildModels(data: SpaceDirectoryState?) {
val host = this val host = this
val results = data?.spaceSummaryApiResult val currentRootId = data?.hierarchyStack?.lastOrNull() ?: data?.spaceId ?: return
val results = data?.apiResults?.get(currentRootId)
if (results is Incomplete) { if (results is Incomplete) {
loadingItem { loadingItem {
@ -94,7 +98,9 @@ class SpaceDirectoryController @Inject constructor(
} }
} }
} else { } else {
val flattenChildInfo = results?.invoke() val hierarchySummary = results?.invoke()
val flattenChildInfo = hierarchySummary
?.children
?.filter { ?.filter {
it.parentRoomId == (data.hierarchyStack.lastOrNull() ?: data.spaceId) it.parentRoomId == (data.hierarchyStack.lastOrNull() ?: data.spaceId)
} }
@ -132,6 +138,7 @@ class SpaceDirectoryController @Inject constructor(
// if it's known use that matrixItem because it would have a better computed name // if it's known use that matrixItem because it would have a better computed name
val matrixItem = data?.knownRoomSummaries?.find { it.roomId == info.childRoomId }?.toMatrixItem() val matrixItem = data?.knownRoomSummaries?.find { it.roomId == info.childRoomId }?.toMatrixItem()
?: info.toMatrixItem() ?: info.toMatrixItem()
spaceChildInfoItem { spaceChildInfoItem {
id(info.childRoomId) id(info.childRoomId)
matrixItem(matrixItem) matrixItem(matrixItem)
@ -162,6 +169,28 @@ class SpaceDirectoryController @Inject constructor(
} }
} }
} }
if (hierarchySummary?.nextToken != null) {
val paginationStatus = data.paginationStatus[currentRootId] ?: Uninitialized
if (paginationStatus is Fail) {
errorWithRetryItem {
id("error_${currentRootId}_${hierarchySummary.nextToken}")
text(host.errorFormatter.toHumanReadable(paginationStatus.error))
listener { host.listener?.retry() }
}
} else {
loadingItem {
id("pagination_${currentRootId}_${hierarchySummary.nextToken}")
showLoader(true)
onVisibilityStateChanged { _, _, visibilityState ->
// Do something with the new visibility state
if (visibilityState == VisibilityState.VISIBLE) {
// we can trigger a seamless load of additional items
host.listener?.loadAdditionalItemsIfNeeded()
}
}
}
}
}
} }
} }
} }

View File

@ -25,6 +25,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -72,6 +73,7 @@ class SpaceDirectoryFragment @Inject constructor(
FragmentSpaceDirectoryBinding.inflate(layoutInflater, container, false) FragmentSpaceDirectoryBinding.inflate(layoutInflater, container, false)
private val viewModel by activityViewModel(SpaceDirectoryViewModel::class) private val viewModel by activityViewModel(SpaceDirectoryViewModel::class)
private val epoxyVisibilityTracker = EpoxyVisibilityTracker()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -84,6 +86,7 @@ class SpaceDirectoryFragment @Inject constructor(
} }
epoxyController.listener = this epoxyController.listener = this
views.spaceDirectoryList.configureWith(epoxyController) views.spaceDirectoryList.configureWith(epoxyController)
epoxyVisibilityTracker.attach(views.spaceDirectoryList)
viewModel.selectSubscribe(this, SpaceDirectoryState::canAddRooms) { viewModel.selectSubscribe(this, SpaceDirectoryState::canAddRooms) {
invalidateOptionsMenu() invalidateOptionsMenu()
@ -95,6 +98,7 @@ class SpaceDirectoryFragment @Inject constructor(
override fun onDestroyView() { override fun onDestroyView() {
epoxyController.listener = null epoxyController.listener = null
epoxyVisibilityTracker.detach(views.spaceDirectoryList)
views.spaceDirectoryList.cleanup() views.spaceDirectoryList.cleanup()
super.onDestroyView() super.onDestroyView()
} }
@ -102,21 +106,20 @@ class SpaceDirectoryFragment @Inject constructor(
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
epoxyController.setData(state) epoxyController.setData(state)
val currentParent = state.hierarchyStack.lastOrNull()?.let { currentParent -> val currentParentId = state.hierarchyStack.lastOrNull()
state.spaceSummaryApiResult.invoke()?.firstOrNull { it.childRoomId == currentParent }
}
if (currentParent == null) { if (currentParentId == null) {
// it's the root
val title = getString(R.string.space_explore_activity_title) val title = getString(R.string.space_explore_activity_title)
views.toolbar.title = title views.toolbar.title = title
spaceCardRenderer.render(state.spaceSummary.invoke(), emptyList(), this, views.spaceCard)
} else { } else {
val title = currentParent.name ?: currentParent.canonicalAlias ?: getString(R.string.space_explore_activity_title) val title = state.currentRootSummary?.name
?: state.currentRootSummary?.canonicalAlias
?: getString(R.string.space_explore_activity_title)
views.toolbar.title = title views.toolbar.title = title
spaceCardRenderer.render(currentParent, emptyList(), this, views.spaceCard)
} }
spaceCardRenderer.render(state.currentRootSummary, emptyList(), this, views.spaceCard)
} }
override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state -> override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state ->
@ -170,6 +173,10 @@ class SpaceDirectoryFragment @Inject constructor(
addExistingRoomActivityResult.launch(SpaceManageActivity.newIntent(requireContext(), spaceId, ManageType.AddRooms)) addExistingRoomActivityResult.launch(SpaceManageActivity.newIntent(requireContext(), spaceId, ManageType.AddRooms))
} }
override fun loadAdditionalItemsIfNeeded() {
viewModel.handle(SpaceDirectoryViewAction.LoadAdditionalItemsIfNeeded)
}
override fun onUrlClicked(url: String, title: String): Boolean { override fun onUrlClicked(url: String, title: String): Boolean {
permalinkHandler permalinkHandler
.launch(requireActivity(), url, null) .launch(requireActivity(), url, null)
@ -206,7 +213,5 @@ class SpaceDirectoryFragment @Inject constructor(
// nothing? // nothing?
return false return false
} }
// override fun navigateToRoom(roomId: String) {
// viewModel.handle(SpaceDirectoryViewAction.NavigateToRoom(roomId))
// }
} }

View File

@ -18,28 +18,27 @@ package im.vector.app.features.spaces.explore
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary 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.model.SpaceChildInfo
import org.matrix.android.sdk.internal.session.space.SpaceHierarchySummary
data class SpaceDirectoryState( data class SpaceDirectoryState(
// The current filter // The current filter
val spaceId: String, val spaceId: String,
val currentFilter: String = "", val currentFilter: String = "",
val spaceSummary: Async<RoomSummary> = Uninitialized, val apiResults: Map<String, Async<SpaceHierarchySummary>> = emptyMap(),
val spaceSummaryApiResult: Async<List<SpaceChildInfo>> = Uninitialized, val currentRootSummary: RoomSummary? = null,
val childList: List<SpaceChildInfo> = emptyList(), val childList: List<SpaceChildInfo> = emptyList(),
val hierarchyStack: List<String> = emptyList(), val hierarchyStack: List<String> = emptyList(),
// True if more result are available server side
val hasMore: Boolean = false,
// Set of joined roomId / spaces, // Set of joined roomId / spaces,
val joinedRoomsIds: Set<String> = emptySet(), val joinedRoomsIds: Set<String> = emptySet(),
// keys are room alias or roomId // keys are room alias or roomId
val changeMembershipStates: Map<String, ChangeMembershipState> = emptyMap(), val changeMembershipStates: Map<String, ChangeMembershipState> = emptyMap(),
val canAddRooms: Boolean = false, val canAddRooms: Boolean = false,
// cached room summaries of known rooms // cached room summaries of known rooms, we use it because computed room name would be better using it
val knownRoomSummaries : List<RoomSummary> = emptyList() val knownRoomSummaries : List<RoomSummary> = emptyList(),
val paginationStatus: Map<String, Async<Unit>> = emptyMap()
) : MvRxState { ) : MvRxState {
constructor(args: SpaceDirectoryArgs) : this( constructor(args: SpaceDirectoryArgs) : this(
spaceId = args.spaceId spaceId = args.spaceId

View File

@ -26,4 +26,5 @@ sealed class SpaceDirectoryViewAction : VectorViewModelAction {
data class NavigateToRoom(val roomId: String) : SpaceDirectoryViewAction() data class NavigateToRoom(val roomId: String) : SpaceDirectoryViewAction()
object HandleBack : SpaceDirectoryViewAction() object HandleBack : SpaceDirectoryViewAction()
object Retry : SpaceDirectoryViewAction() object Retry : SpaceDirectoryViewAction()
object LoadAdditionalItemsIfNeeded : SpaceDirectoryViewAction()
} }

View File

@ -23,6 +23,7 @@ import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@ -34,6 +35,8 @@ import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
@ -67,11 +70,11 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
childList = spaceSum?.spaceChildren ?: emptyList(), childList = spaceSum?.spaceChildren ?: emptyList(),
spaceSummary = spaceSum?.let { Success(spaceSum) } ?: Loading() currentRootSummary = spaceSum
) )
} }
refreshFromApi() refreshFromApi(initialState.spaceId)
observeJoinedRooms() observeJoinedRooms()
observeMembershipChanges() observeMembershipChanges()
observePermissions() observePermissions()
@ -93,29 +96,44 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
.disposeOnClear() .disposeOnClear()
} }
private fun refreshFromApi() { private fun refreshFromApi(rootId: String?) = withState { state ->
val spaceId = rootId ?: initialState.spaceId
setState { setState {
copy( copy(
spaceSummaryApiResult = Loading() apiResults = state.apiResults.toMutableMap().apply {
this[spaceId] = Loading()
}.toMap()
) )
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val cachedResults = state.apiResults.toMutableMap()
try { try {
val query = session.spaceService().querySpaceChildren(initialState.spaceId) val query = session.spaceService().querySpaceChildren(
val knownSummaries = query.second.mapNotNull { spaceId,
limit = 10
)
val knownSummaries = query.children.mapNotNull {
session.getRoomSummary(it.childRoomId) session.getRoomSummary(it.childRoomId)
?.takeIf { it.membership == Membership.JOIN } // only take if joined because it will be up to date (synced) ?.takeIf { it.membership == Membership.JOIN } // only take if joined because it will be up to date (synced)
} }.distinctBy { it.roomId }
setState { setState {
copy( copy(
spaceSummaryApiResult = Success(query.second), apiResults = cachedResults.apply {
knownRoomSummaries = knownSummaries this[spaceId] = Success(query)
},
currentRootSummary = query.rootSummary,
paginationStatus = state.paginationStatus.toMutableMap().apply {
this[spaceId] = Uninitialized
}.toMap(),
knownRoomSummaries = (state.knownRoomSummaries + knownSummaries).distinctBy { it.roomId },
) )
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
setState { setState {
copy( copy(
spaceSummaryApiResult = Fail(failure) apiResults = cachedResults.apply {
this[spaceId] = Fail(failure)
}
) )
} }
} }
@ -149,39 +167,143 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
override fun handle(action: SpaceDirectoryViewAction) { override fun handle(action: SpaceDirectoryViewAction) {
when (action) { when (action) {
is SpaceDirectoryViewAction.ExploreSubSpace -> { is SpaceDirectoryViewAction.ExploreSubSpace -> {
setState { handleExploreSubSpace(action)
copy(hierarchyStack = hierarchyStack + listOf(action.spaceChildInfo.childRoomId))
}
} }
SpaceDirectoryViewAction.HandleBack -> { SpaceDirectoryViewAction.HandleBack -> {
withState { handleBack()
if (it.hierarchyStack.isEmpty()) {
_viewEvents.post(SpaceDirectoryViewEvents.Dismiss)
} else {
setState {
copy(
hierarchyStack = hierarchyStack.dropLast(1)
)
}
}
}
} }
is SpaceDirectoryViewAction.JoinOrOpen -> { is SpaceDirectoryViewAction.JoinOrOpen -> {
handleJoinOrOpen(action.spaceChildInfo) handleJoinOrOpen(action.spaceChildInfo)
} }
is SpaceDirectoryViewAction.NavigateToRoom -> { is SpaceDirectoryViewAction.NavigateToRoom -> {
_viewEvents.post(SpaceDirectoryViewEvents.NavigateToRoom(action.roomId)) _viewEvents.post(SpaceDirectoryViewEvents.NavigateToRoom(action.roomId))
} }
is SpaceDirectoryViewAction.ShowDetails -> { is SpaceDirectoryViewAction.ShowDetails -> {
// This is temporary for now to at least display something for the space beta // This is temporary for now to at least display something for the space beta
// It's not ideal as it's doing some peeking that is not needed. // It's not ideal as it's doing some peeking that is not needed.
session.permalinkService().createRoomPermalink(action.spaceChildInfo.childRoomId)?.let { session.permalinkService().createRoomPermalink(action.spaceChildInfo.childRoomId)?.let {
_viewEvents.post(SpaceDirectoryViewEvents.NavigateToMxToBottomSheet(it)) _viewEvents.post(SpaceDirectoryViewEvents.NavigateToMxToBottomSheet(it))
} }
} }
SpaceDirectoryViewAction.Retry -> { SpaceDirectoryViewAction.Retry -> {
refreshFromApi() handleRetry()
}
SpaceDirectoryViewAction.LoadAdditionalItemsIfNeeded -> {
loadAdditionalItemsIfNeeded()
}
}
}
private fun handleBack() = withState { state ->
if (state.hierarchyStack.isEmpty()) {
_viewEvents.post(SpaceDirectoryViewEvents.Dismiss)
} else {
val newStack = state.hierarchyStack.dropLast(1)
val newRootId = newStack.lastOrNull() ?: initialState.spaceId
val rootSummary = state.apiResults[newRootId]?.invoke()?.rootSummary
setState {
copy(
hierarchyStack = hierarchyStack.dropLast(1),
currentRootSummary = rootSummary
)
}
}
}
private fun handleRetry() = withState { state ->
refreshFromApi(state.hierarchyStack.lastOrNull() ?: initialState.spaceId)
}
private fun handleExploreSubSpace(action: SpaceDirectoryViewAction.ExploreSubSpace) = withState { state ->
val newRootId = action.spaceChildInfo.childRoomId
val curSum = RoomSummary(
roomId = action.spaceChildInfo.childRoomId,
roomType = action.spaceChildInfo.roomType,
name = action.spaceChildInfo.name ?: "",
canonicalAlias = action.spaceChildInfo.canonicalAlias,
topic = action.spaceChildInfo.topic ?: "",
joinedMembersCount = action.spaceChildInfo.activeMemberCount,
avatarUrl = action.spaceChildInfo.avatarUrl ?: "",
isEncrypted = false,
joinRules = if (action.spaceChildInfo.worldReadable) RoomJoinRules.PUBLIC else RoomJoinRules.PRIVATE,
encryptionEventTs = 0,
typingUsers = emptyList()
)
setState {
copy(
hierarchyStack = hierarchyStack + listOf(newRootId),
currentRootSummary = curSum
)
}
val shouldLoad = when (state.apiResults[newRootId]) {
Uninitialized -> true
is Loading -> false
is Success -> false
is Fail -> true
null -> true
}
if (shouldLoad) {
refreshFromApi(newRootId)
}
}
private fun loadAdditionalItemsIfNeeded() = withState { state ->
val currentRootId = state.hierarchyStack.lastOrNull() ?: initialState.spaceId
val mutablePaginationStatus = state.paginationStatus.toMutableMap().apply {
if (this[currentRootId] == null) {
this[currentRootId] = Uninitialized
}
}
if (mutablePaginationStatus[currentRootId] is Loading) return@withState
setState {
copy(paginationStatus = mutablePaginationStatus.toMap())
}
viewModelScope.launch(Dispatchers.IO) {
val cachedResults = state.apiResults.toMutableMap()
try {
val currentResponse = cachedResults[currentRootId]?.invoke()
if (currentResponse == null) {
// nothing to paginate through...
setState {
copy(paginationStatus = mutablePaginationStatus.apply { this[currentRootId] = Uninitialized }.toMap())
}
return@launch
}
val query = session.spaceService().querySpaceChildren(
currentRootId,
limit = 10,
from = currentResponse.nextToken,
knownStateList = currentResponse.childrenState
)
val knownSummaries = query.children.mapNotNull {
session.getRoomSummary(it.childRoomId)
?.takeIf { it.membership == Membership.JOIN } // only take if joined because it will be up to date (synced)
}.distinctBy { it.roomId }
cachedResults[currentRootId] = Success(
currentResponse.copy(
children = currentResponse.children + query.children,
nextToken = query.nextToken,
)
)
setState {
copy(
apiResults = cachedResults.toMap(),
paginationStatus = mutablePaginationStatus.apply { this[currentRootId] = Success(Unit) }.toMap(),
knownRoomSummaries = (state.knownRoomSummaries + knownSummaries).distinctBy { it.roomId }
)
}
} catch (failure: Throwable) {
setState {
copy(
paginationStatus = mutablePaginationStatus.apply { this[currentRootId] = Fail(failure) }.toMap()
)
}
} }
} }
} }

View File

@ -50,7 +50,7 @@ class SpaceManageRoomsViewModel @AssistedInject constructor(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val apiResult = runCatchingToAsync { val apiResult = runCatchingToAsync {
session.spaceService().querySpaceChildren(spaceId = initialState.spaceId).second session.spaceService().querySpaceChildren(spaceId = initialState.spaceId).children
} }
setState { setState {
copy( copy(
@ -131,8 +131,8 @@ class SpaceManageRoomsViewModel @AssistedInject constructor(
roomId = info.childRoomId, roomId = info.childRoomId,
viaServers = info.viaServers, viaServers = info.viaServers,
order = info.order, order = info.order,
suggested = suggested, suggested = suggested
autoJoin = info.autoJoin // autoJoin = info.autoJoin
) )
} catch (failure: Throwable) { } catch (failure: Throwable) {
errorList.add(failure) errorList.add(failure)
@ -156,7 +156,7 @@ class SpaceManageRoomsViewModel @AssistedInject constructor(
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val apiResult = runCatchingToAsync { val apiResult = runCatchingToAsync {
session.spaceService().querySpaceChildren(spaceId = initialState.spaceId).second session.spaceService().querySpaceChildren(spaceId = initialState.spaceId).children
} }
setState { setState {
copy( copy(

View File

@ -151,7 +151,7 @@ class SpacePreviewViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
spaceInfo = Success( spaceInfo = Success(
resolveResult.first.let { resolveResult.rootSummary.let {
ChildInfo( ChildInfo(
roomId = it.roomId, roomId = it.roomId,
avatarUrl = it.avatarUrl, avatarUrl = it.avatarUrl,
@ -165,7 +165,7 @@ class SpacePreviewViewModel @AssistedInject constructor(
} }
), ),
childInfoList = Success( childInfoList = Success(
resolveResult.second.map { resolveResult.children.map {
ChildInfo( ChildInfo(
roomId = it.childRoomId, roomId = it.childRoomId,
avatarUrl = it.avatarUrl, avatarUrl = it.avatarUrl,