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 order: String?,
val activeMemberCount: Int?,
val autoJoin: Boolean,
// val autoJoin: Boolean,
val viaServers: List<String>,
val parentRoomId: String?,
val suggested: Boolean?,

View File

@ -36,7 +36,7 @@ interface Space {
suspend fun addChildren(roomId: String,
viaServers: List<String>?,
order: String?,
autoJoin: Boolean = false,
// autoJoin: Boolean = false,
suggested: Boolean? = false)
fun getChildInfo(roomId: String): SpaceChildContent?
@ -46,8 +46,8 @@ interface Space {
@Throws
suspend fun setChildrenOrder(roomId: String, order: String?)
@Throws
suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean)
// @Throws
// suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean)
@Throws
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 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.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
typealias SpaceSummaryQueryParams = RoomSummaryQueryParams
@ -58,10 +59,18 @@ interface SpaceService {
/**
* 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,
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.

View File

@ -40,12 +40,12 @@ data class SpaceChildContent(
* or consist of more than 50 characters, are forbidden and should be ignored if received.)
*/
@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
* 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.)
*/
@Json(name = "auto_join") val autoJoin: Boolean? = false,
// /**
// * 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.
// * (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,
/**
* 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,
activeMemberCount = it.childSummaryEntity?.joinedMembersCount,
order = it.order,
autoJoin = it.autoJoin ?: false,
// autoJoin = it.autoJoin ?: false,
viaServers = it.viaServers.toList(),
parentRoomId = roomSummaryEntity.roomId,
suggested = it.suggested,

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.space
import android.net.Uri
import androidx.lifecycle.LiveData
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.toContent
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.RoomDirectoryVisibility
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.SpaceChildInfo
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,
suggestedOnly: Boolean?,
autoJoinedOnly: Boolean?): Pair<RoomSummary, List<SpaceChildInfo>> {
return resolveSpaceInfoTask.execute(ResolveSpaceInfoTask.Params.withId(spaceId, suggestedOnly, autoJoinedOnly)).let { response ->
limit: Int?,
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 }
Pair(
first = RoomSummary(
roomId = spaceDesc?.roomId ?: spaceId,
roomType = spaceDesc?.roomType,
name = spaceDesc?.name ?: "",
displayName = spaceDesc?.name ?: "",
topic = spaceDesc?.topic ?: "",
joinedMembersCount = spaceDesc?.numJoinedMembers,
avatarUrl = spaceDesc?.avatarUrl ?: "",
encryptionEventTs = null,
typingUsers = emptyList(),
isEncrypted = false,
flattenParentIds = emptyList()
),
second = response.rooms
?.filter { it.roomId != spaceId }
?.flatMap { childSummary ->
response.events
?.filter { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD }
?.mapNotNull { childStateEv ->
// create a child entry for everytime this room is the child of a space
// beware that a room could appear then twice in this list
childStateEv.content.toModel<SpaceChildContent>()?.let { childStateEvContent ->
SpaceChildInfo(
childRoomId = childSummary.roomId,
isKnown = true,
roomType = childSummary.roomType,
name = childSummary.name,
topic = childSummary.topic,
avatarUrl = childSummary.avatarUrl,
order = childStateEvContent.order,
autoJoin = childStateEvContent.autoJoin ?: false,
viaServers = childStateEvContent.via.orEmpty(),
activeMemberCount = childSummary.numJoinedMembers,
parentRoomId = childStateEv.roomId,
suggested = childStateEvContent.suggested,
canonicalAlias = childSummary.canonicalAlias,
aliases = childSummary.aliases,
worldReadable = childSummary.worldReadable
)
}
}.orEmpty()
}
.orEmpty()
val root = RoomSummary(
roomId = spaceDesc?.roomId ?: spaceId,
roomType = spaceDesc?.roomType,
name = spaceDesc?.name ?: "",
displayName = spaceDesc?.name ?: "",
topic = spaceDesc?.topic ?: "",
joinedMembersCount = spaceDesc?.numJoinedMembers,
avatarUrl = spaceDesc?.avatarUrl ?: "",
encryptionEventTs = null,
typingUsers = emptyList(),
isEncrypted = false,
flattenParentIds = emptyList(),
canonicalAlias = spaceDesc?.canonicalAlias,
joinRules = RoomJoinRules.PUBLIC.takeIf { spaceDesc?.worldReadable == true }
)
val children = response.rooms
?.filter { it.roomId != spaceId }
?.flatMap { childSummary ->
(spaceDesc?.childrenState ?: knownStateList)
?.filter { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD }
?.mapNotNull { childStateEv ->
// create a child entry for everytime this room is the child of a space
// beware that a room could appear then twice in this list
childStateEv.content.toModel<SpaceChildContent>()?.let { childStateEvContent ->
SpaceChildInfo(
childRoomId = childSummary.roomId,
isKnown = true,
roomType = childSummary.roomType,
name = childSummary.name,
topic = childSummary.topic,
avatarUrl = childSummary.avatarUrl,
order = childStateEvContent.order,
// autoJoin = childStateEvContent.autoJoin ?: false,
viaServers = childStateEvContent.via.orEmpty(),
activeMemberCount = childSummary.numJoinedMembers,
parentRoomId = childStateEv.roomId,
suggested = childStateEvContent.suggested,
canonicalAlias = childSummary.canonicalAlias,
aliases = childSummary.aliases,
worldReadable = childSummary.worldReadable
)
}
}.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 kotlinx.coroutines.TimeoutCancellationException
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.internal.database.awaitNotEmptyResult
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)
val summary = roomSummaryDataSource.getSpaceSummary(params.roomIdOrAlias)
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
Timber.v("## Space: Processing child :[${it.childRoomId}] autoJoin:${it.autoJoin}")
if (it.autoJoin) {
// I should try to join as well
if (it.roomType == RoomType.SPACE) {
// recursively join auto-joined child of this space?
when (val subspaceJoinResult = execute(JoinSpaceTask.Params(it.childRoomId, null, it.viaServers))) {
JoinSpaceResult.Success -> {
// nop
}
is JoinSpaceResult.Fail -> {
errors[it.childRoomId] = subspaceJoinResult.error
}
is JoinSpaceResult.PartialSuccess -> {
errors.putAll(subspaceJoinResult.failedRooms)
}
}
} else {
try {
Timber.v("## Space: Joining room child ${it.childRoomId}")
joinRoomTask.execute(JoinRoomTask.Params(
roomIdOrAlias = it.childRoomId,
reason = "Auto-join parent space",
viaServers = it.viaServers
))
} catch (failure: Throwable) {
errors[it.childRoomId] = failure
Timber.e("## Space: Failed to join room child ${it.childRoomId}")
}
}
}
}
// Timber.v("## Space: Processing child :[${it.childRoomId}] suggested:${it.suggested}")
// if (it.autoJoin) {
// // I should try to join as well
// if (it.roomType == RoomType.SPACE) {
// // recursively join auto-joined child of this space?
// when (val subspaceJoinResult = execute(JoinSpaceTask.Params(it.childRoomId, null, it.viaServers))) {
// JoinSpaceResult.Success -> {
// // nop
// }
// is JoinSpaceResult.Fail -> {
// errors[it.childRoomId] = subspaceJoinResult.error
// }
// is JoinSpaceResult.PartialSuccess -> {
// errors.putAll(subspaceJoinResult.failedRooms)
// }
// }
// } else {
// try {
// Timber.v("## Space: Joining room child ${it.childRoomId}")
// joinRoomTask.execute(JoinRoomTask.Params(
// roomIdOrAlias = it.childRoomId,
// reason = "Auto-join parent space",
// viaServers = it.viaServers
// ))
// } catch (failure: Throwable) {
// errors[it.childRoomId] = failure
// Timber.e("## Space: Failed to join room child ${it.childRoomId}")
// }
// }
// }
// }
return if (errors.isEmpty()) {
JoinSpaceResult.Success

View File

@ -24,23 +24,24 @@ import javax.inject.Inject
internal interface ResolveSpaceInfoTask : Task<ResolveSpaceInfoTask.Params, SpacesResponse> {
data class Params(
val spaceId: String,
val maxRoomPerSpace: Int?,
val limit: Int,
val batchToken: String?,
val suggestedOnly: Boolean?,
val autoJoinOnly: Boolean?
// val maxRoomPerSpace: Int?,
val limit: Int?,
val maxDepth: Int?,
val from: String?,
val suggestedOnly: Boolean?
// val autoJoinOnly: Boolean?
) {
companion object {
fun withId(spaceId: String, suggestedOnly: Boolean?, autoJoinOnly: Boolean?) =
Params(
spaceId = spaceId,
maxRoomPerSpace = 10,
limit = 20,
batchToken = null,
suggestedOnly = suggestedOnly,
autoJoinOnly = autoJoinOnly
)
}
// companion object {
// fun withId(spaceId: String, suggestedOnly: Boolean?) =
// Params(
// spaceId = spaceId,
// // maxRoomPerSpace = 10,
// limit = 20,
// from = null,
// suggestedOnly = suggestedOnly
// // autoJoinOnly = autoJoinOnly
// )
// }
}
}
@ -49,15 +50,13 @@ internal class DefaultResolveSpaceInfoTask @Inject constructor(
private val globalErrorReceiver: GlobalErrorReceiver
) : ResolveSpaceInfoTask {
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) {
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
import org.matrix.android.sdk.internal.network.NetworkConstants
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
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
* - https://hackmd.io/fNYh4tjUT5mQfR1uuRzWDA
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/spaces")
suspend fun getSpaces(@Path("roomId") spaceId: String,
@Body params: SpaceSummaryParams): SpacesResponse
// @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/spaces")
// suspend fun getSpaces(@Path("roomId") spaceId: String,
// @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.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)
internal data class SpaceChildSummaryResponse(
/**
* 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.
*/
@Json(name = "num_refs") val numRefs: Int? = null,
// /**
// * 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.
// */
// @Json(name = "num_refs") val numRefs: Int? = null,
/**
* 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,
/** 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.
*/

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.JsonClass
import org.matrix.android.sdk.api.session.events.model.Event
@JsonClass(generateAdapter = true)
internal data class SpacesResponse(
/** Its presence indicates that there are more results to return. */
@Json(name = "next_batch") val nextBatch: String? = null,
/** Rooms information like name/avatar/type ... */
@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
@Json(name = "rooms") val rooms: List<SpaceChildSummaryResponse>? = null
)

View File

@ -103,7 +103,7 @@ internal class DefaultPeekSpaceTask @Inject constructor(
// can't peek :/
spaceChildResults.add(
SpaceChildPeekResult(
childId, childPeek, entry.second?.autoJoin, entry.second?.order
childId, childPeek, entry.second?.order
)
)
// continue to next child
@ -116,7 +116,7 @@ internal class DefaultPeekSpaceTask @Inject constructor(
SpaceSubChildPeekResult(
childId,
childPeek,
entry.second?.autoJoin,
// entry.second?.autoJoin,
entry.second?.order,
peekChildren(childStateEvents, depth + 1, maxDepth)
)
@ -127,7 +127,7 @@ internal class DefaultPeekSpaceTask @Inject constructor(
Timber.v("## SPACE_PEEK: room child $entry")
spaceChildResults.add(
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 {
val id: String
val roomPeekResult: PeekResult
val default: Boolean?
// val default: Boolean?
val order: String?
}
data class SpaceChildPeekResult(
override val id: String,
override val roomPeekResult: PeekResult,
override val default: Boolean? = null,
// override val default: Boolean? = null,
override val order: String? = null
) : ISpaceChild
data class SpaceSubChildPeekResult(
override val id: String,
override val roomPeekResult: PeekResult,
override val default: Boolean?,
// override val default: Boolean?,
override val order: String?,
val children: List<ISpaceChild>
) : ISpaceChild

View File

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

View File

@ -230,8 +230,11 @@ class RoomListSectionBuilderSpace(
Observable.just(emptyList())
} else {
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
val spaceSum = tryOrNull { session.spaceService().querySpaceChildren(selectedSpace.roomId, suggestedOnly = true) }
val value = spaceSum?.second.orEmpty().distinctBy { it.childRoomId }
val spaceSum = tryOrNull {
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.
val filtered = value.filter {
session.getRoomSummary(it.childRoomId)?.membership?.isActive() != true

View File

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

View File

@ -134,7 +134,7 @@ class CreateSpaceViewModelTask @Inject constructor(
timeout.roomID
}
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
session.spaceService().setSpaceParent(
roomId,

View File

@ -18,8 +18,10 @@ package im.vector.app.features.spaces.explore
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.epoxy.VisibilityState
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.errorWithRetryItem
@ -54,13 +56,15 @@ class SpaceDirectoryController @Inject constructor(
fun onRoomClick(spaceChildInfo: SpaceChildInfo)
fun retry()
fun addExistingRooms(spaceId: String)
fun loadAdditionalItemsIfNeeded()
}
var listener: InteractionListener? = null
override fun buildModels(data: SpaceDirectoryState?) {
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) {
loadingItem {
@ -94,7 +98,9 @@ class SpaceDirectoryController @Inject constructor(
}
}
} else {
val flattenChildInfo = results?.invoke()
val hierarchySummary = results?.invoke()
val flattenChildInfo = hierarchySummary
?.children
?.filter {
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
val matrixItem = data?.knownRoomSummaries?.find { it.roomId == info.childRoomId }?.toMatrixItem()
?: info.toMatrixItem()
spaceChildInfoItem {
id(info.childRoomId)
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 androidx.core.text.toSpannable
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -72,6 +73,7 @@ class SpaceDirectoryFragment @Inject constructor(
FragmentSpaceDirectoryBinding.inflate(layoutInflater, container, false)
private val viewModel by activityViewModel(SpaceDirectoryViewModel::class)
private val epoxyVisibilityTracker = EpoxyVisibilityTracker()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -84,6 +86,7 @@ class SpaceDirectoryFragment @Inject constructor(
}
epoxyController.listener = this
views.spaceDirectoryList.configureWith(epoxyController)
epoxyVisibilityTracker.attach(views.spaceDirectoryList)
viewModel.selectSubscribe(this, SpaceDirectoryState::canAddRooms) {
invalidateOptionsMenu()
@ -95,6 +98,7 @@ class SpaceDirectoryFragment @Inject constructor(
override fun onDestroyView() {
epoxyController.listener = null
epoxyVisibilityTracker.detach(views.spaceDirectoryList)
views.spaceDirectoryList.cleanup()
super.onDestroyView()
}
@ -102,21 +106,20 @@ class SpaceDirectoryFragment @Inject constructor(
override fun invalidate() = withState(viewModel) { state ->
epoxyController.setData(state)
val currentParent = state.hierarchyStack.lastOrNull()?.let { currentParent ->
state.spaceSummaryApiResult.invoke()?.firstOrNull { it.childRoomId == currentParent }
}
val currentParentId = state.hierarchyStack.lastOrNull()
if (currentParent == null) {
if (currentParentId == null) {
// it's the root
val title = getString(R.string.space_explore_activity_title)
views.toolbar.title = title
spaceCardRenderer.render(state.spaceSummary.invoke(), emptyList(), this, views.spaceCard)
} 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
spaceCardRenderer.render(currentParent, emptyList(), this, views.spaceCard)
}
spaceCardRenderer.render(state.currentRootSummary, emptyList(), this, views.spaceCard)
}
override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state ->
@ -170,6 +173,10 @@ class SpaceDirectoryFragment @Inject constructor(
addExistingRoomActivityResult.launch(SpaceManageActivity.newIntent(requireContext(), spaceId, ManageType.AddRooms))
}
override fun loadAdditionalItemsIfNeeded() {
viewModel.handle(SpaceDirectoryViewAction.LoadAdditionalItemsIfNeeded)
}
override fun onUrlClicked(url: String, title: String): Boolean {
permalinkHandler
.launch(requireActivity(), url, null)
@ -206,7 +213,5 @@ class SpaceDirectoryFragment @Inject constructor(
// nothing?
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.MvRxState
import com.airbnb.mvrx.Uninitialized
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.SpaceChildInfo
import org.matrix.android.sdk.internal.session.space.SpaceHierarchySummary
data class SpaceDirectoryState(
// The current filter
val spaceId: String,
val currentFilter: String = "",
val spaceSummary: Async<RoomSummary> = Uninitialized,
val spaceSummaryApiResult: Async<List<SpaceChildInfo>> = Uninitialized,
val apiResults: Map<String, Async<SpaceHierarchySummary>> = emptyMap(),
val currentRootSummary: RoomSummary? = null,
val childList: List<SpaceChildInfo> = emptyList(),
val hierarchyStack: List<String> = emptyList(),
// True if more result are available server side
val hasMore: Boolean = false,
// Set of joined roomId / spaces,
val joinedRoomsIds: Set<String> = emptySet(),
// keys are room alias or roomId
val changeMembershipStates: Map<String, ChangeMembershipState> = emptyMap(),
val canAddRooms: Boolean = false,
// cached room summaries of known rooms
val knownRoomSummaries : List<RoomSummary> = emptyList()
// cached room summaries of known rooms, we use it because computed room name would be better using it
val knownRoomSummaries : List<RoomSummary> = emptyList(),
val paginationStatus: Map<String, Async<Unit>> = emptyMap()
) : MvRxState {
constructor(args: SpaceDirectoryArgs) : this(
spaceId = args.spaceId

View File

@ -26,4 +26,5 @@ sealed class SpaceDirectoryViewAction : VectorViewModelAction {
data class NavigateToRoom(val roomId: String) : SpaceDirectoryViewAction()
object HandleBack : 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.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
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.events.model.EventType
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.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
@ -67,11 +70,11 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
setState {
copy(
childList = spaceSum?.spaceChildren ?: emptyList(),
spaceSummary = spaceSum?.let { Success(spaceSum) } ?: Loading()
currentRootSummary = spaceSum
)
}
refreshFromApi()
refreshFromApi(initialState.spaceId)
observeJoinedRooms()
observeMembershipChanges()
observePermissions()
@ -93,29 +96,44 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
.disposeOnClear()
}
private fun refreshFromApi() {
private fun refreshFromApi(rootId: String?) = withState { state ->
val spaceId = rootId ?: initialState.spaceId
setState {
copy(
spaceSummaryApiResult = Loading()
apiResults = state.apiResults.toMutableMap().apply {
this[spaceId] = Loading()
}.toMap()
)
}
viewModelScope.launch(Dispatchers.IO) {
val cachedResults = state.apiResults.toMutableMap()
try {
val query = session.spaceService().querySpaceChildren(initialState.spaceId)
val knownSummaries = query.second.mapNotNull {
val query = session.spaceService().querySpaceChildren(
spaceId,
limit = 10
)
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 }
setState {
copy(
spaceSummaryApiResult = Success(query.second),
knownRoomSummaries = knownSummaries
apiResults = cachedResults.apply {
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) {
setState {
copy(
spaceSummaryApiResult = Fail(failure)
apiResults = cachedResults.apply {
this[spaceId] = Fail(failure)
}
)
}
}
@ -149,39 +167,143 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
override fun handle(action: SpaceDirectoryViewAction) {
when (action) {
is SpaceDirectoryViewAction.ExploreSubSpace -> {
setState {
copy(hierarchyStack = hierarchyStack + listOf(action.spaceChildInfo.childRoomId))
}
is SpaceDirectoryViewAction.ExploreSubSpace -> {
handleExploreSubSpace(action)
}
SpaceDirectoryViewAction.HandleBack -> {
withState {
if (it.hierarchyStack.isEmpty()) {
_viewEvents.post(SpaceDirectoryViewEvents.Dismiss)
} else {
setState {
copy(
hierarchyStack = hierarchyStack.dropLast(1)
)
}
}
}
SpaceDirectoryViewAction.HandleBack -> {
handleBack()
}
is SpaceDirectoryViewAction.JoinOrOpen -> {
is SpaceDirectoryViewAction.JoinOrOpen -> {
handleJoinOrOpen(action.spaceChildInfo)
}
is SpaceDirectoryViewAction.NavigateToRoom -> {
is SpaceDirectoryViewAction.NavigateToRoom -> {
_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
// It's not ideal as it's doing some peeking that is not needed.
session.permalinkService().createRoomPermalink(action.spaceChildInfo.childRoomId)?.let {
_viewEvents.post(SpaceDirectoryViewEvents.NavigateToMxToBottomSheet(it))
}
}
SpaceDirectoryViewAction.Retry -> {
refreshFromApi()
SpaceDirectoryViewAction.Retry -> {
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) {
val apiResult = runCatchingToAsync {
session.spaceService().querySpaceChildren(spaceId = initialState.spaceId).second
session.spaceService().querySpaceChildren(spaceId = initialState.spaceId).children
}
setState {
copy(
@ -131,8 +131,8 @@ class SpaceManageRoomsViewModel @AssistedInject constructor(
roomId = info.childRoomId,
viaServers = info.viaServers,
order = info.order,
suggested = suggested,
autoJoin = info.autoJoin
suggested = suggested
// autoJoin = info.autoJoin
)
} catch (failure: Throwable) {
errorList.add(failure)
@ -156,7 +156,7 @@ class SpaceManageRoomsViewModel @AssistedInject constructor(
}
viewModelScope.launch(Dispatchers.IO) {
val apiResult = runCatchingToAsync {
session.spaceService().querySpaceChildren(spaceId = initialState.spaceId).second
session.spaceService().querySpaceChildren(spaceId = initialState.spaceId).children
}
setState {
copy(

View File

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