diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt index 4043a3f7b4..304fb4d98b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -18,6 +18,8 @@ package org.matrix.android.sdk.api.session.space import androidx.lifecycle.LiveData 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.peeking.SpacePeekResult typealias SpaceSummaryQueryParams = RoomSummaryQueryParams @@ -43,6 +45,11 @@ interface SpaceService { */ suspend fun peekSpace(spaceId: String) : SpacePeekResult + /** + * Get's information of a space by querying the server + */ + suspend fun querySpaceChildren(spaceId: String) : Pair> + /** * Get a live list of space summaries. This list is refreshed as soon as the data changes. * @return the [LiveData] of List[SpaceSummary] @@ -69,4 +76,6 @@ interface SpaceService { reason: String? = null, viaServers: List = emptyList(), autoJoinChild: List) : JoinSpaceResult + + suspend fun rejectInvite(spaceId: String, reason: String?) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index 7e1e3d0f70..541c877b1d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -52,6 +52,7 @@ import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker import org.matrix.android.sdk.internal.session.room.send.SendEventWorker import org.matrix.android.sdk.internal.session.search.SearchModule import org.matrix.android.sdk.internal.session.signout.SignOutModule +import org.matrix.android.sdk.internal.session.space.SpaceModule import org.matrix.android.sdk.internal.session.sync.SyncModule import org.matrix.android.sdk.internal.session.sync.SyncTask import org.matrix.android.sdk.internal.session.sync.SyncTokenStore @@ -91,7 +92,8 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers FederationModule::class, CallModule::class, SearchModule::class, - ThirdPartyModule::class + ThirdPartyModule::class, + SpaceModule::class ] ) @SessionScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 8cc7f41d5b..c19c59e6db 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -90,11 +90,7 @@ import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask import org.matrix.android.sdk.internal.session.room.uploads.DefaultGetUploadsTask import org.matrix.android.sdk.internal.session.room.uploads.GetUploadsTask -import org.matrix.android.sdk.internal.session.space.DefaultJoinSpaceTask import org.matrix.android.sdk.internal.session.space.DefaultSpaceService -import org.matrix.android.sdk.internal.session.space.JoinSpaceTask -import org.matrix.android.sdk.internal.session.space.peeking.DefaultPeekSpaceTask -import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask import retrofit2.Retrofit @Module diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index 973904cbd5..42f4ed4742 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -18,12 +18,17 @@ package org.matrix.android.sdk.internal.session.space import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +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.space.CreateSpaceParams import org.matrix.android.sdk.api.session.space.Space import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams +import org.matrix.android.sdk.api.session.space.model.SpaceChildContent import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.RoomGetter import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask @@ -31,6 +36,7 @@ import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask +import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult @@ -50,6 +56,8 @@ internal class DefaultSpaceService @Inject constructor( private val roomGetter: RoomGetter, private val spaceSummaryDataSource: SpaceSummaryDataSource, private val peekSpaceTask: PeekSpaceTask, + private val resolveSpaceInfoTask: ResolveSpaceInfoTask, + private val leaveRoomTask: LeaveRoomTask, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, private val taskExecutor: TaskExecutor ) : SpaceService { @@ -76,7 +84,55 @@ internal class DefaultSpaceService @Inject constructor( return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId)) } - override suspend fun joinSpace(spaceIdOrAlias: String, reason: String?, viaServers: List, autoJoinChild: List): SpaceService.JoinSpaceResult { + override suspend fun querySpaceChildren(spaceId: String): Pair> { + return resolveSpaceInfoTask.execute(ResolveSpaceInfoTask.Params.withId(spaceId)).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 + ), + second = response.rooms + ?.filter { it.roomId != spaceId } + ?.map { childSummary -> + val childStateEv = response.events + ?.firstOrNull { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD } + ?.content.toModel() + SpaceChildInfo( + roomSummary = RoomSummary( + roomId = childSummary.roomId, + roomType = childSummary.roomType, + name = childSummary.name ?: "", + displayName = childSummary.name ?: "", + topic = childSummary.topic ?: "", + joinedMembersCount = childSummary.numJoinedMembers, + avatarUrl = childSummary.avatarUrl ?: "", + encryptionEventTs = null, + typingUsers = emptyList(), + isEncrypted = false + ), + order = childStateEv?.order, + present = childStateEv?.present ?: false, + autoJoin = childStateEv?.default ?: false, + viaServers = childStateEv?.via ?: emptyList() + ) + } ?: emptyList() + ) + } + } + + override suspend fun joinSpace(spaceIdOrAlias: String, + reason: String?, + viaServers: List, + autoJoinChild: List): SpaceService.JoinSpaceResult { try { joinSpaceTask.execute(JoinSpaceTask.Params(spaceIdOrAlias, reason, viaServers)) // TODO partial success @@ -100,4 +156,8 @@ internal class DefaultSpaceService @Inject constructor( return SpaceService.JoinSpaceResult.Fail(throwable) } } + + override suspend fun rejectInvite(spaceId: String, reason: String?) { + leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason)) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt index 82dfb55dcc..1878d2c0f9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * 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. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt new file mode 100644 index 0000000000..1eacdce8df --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt @@ -0,0 +1,45 @@ +/* + * 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 org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface ResolveSpaceInfoTask : Task { + data class Params( + val spaceId: String, + val maxRoomPerSpace: Int, + val limit: Int, + val batchToken: String? + ) { + companion object { + fun withId(spaceId: String) = Params(spaceId, 10, 20, null) + } + } +} + +internal class DefaultResolveSpaceInfoTask @Inject constructor( + private val spaceApi: SpaceApi +) : ResolveSpaceInfoTask { + override suspend fun execute(params: ResolveSpaceInfoTask.Params): SpacesResponse { + val body = SpaceSummaryParams(maxRoomPerSpace = params.maxRoomPerSpace, limit = params.limit, batch = params.batchToken ?: "") + return executeRequest(null) { + apiCall = spaceApi.getSpaces(params.spaceId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt new file mode 100644 index 0000000000..5919a90b99 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt @@ -0,0 +1,42 @@ +/* + * 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 org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Path + +internal interface SpaceApi { + + /** + * + * POST /_matrix/client/r0/rooms/{roomID}/spaces + * { + * "max_rooms_per_space": 5, // The maximum number of rooms/subspaces to return for a given space, if negative unbounded. default: -1. + * "limit": 100, // The maximum number of rooms/subspaces to return, server can override this, default: 100. + * "batch": "opaque_string" // A token to use if this is a subsequent HTTP hit, default: "". + * } + * + * MSC 2946 https://github.com/matrix-org/matrix-doc/blob/kegan/spaces-summary/proposals/2946-spaces-summary.md + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/spaces") + fun getSpaces(@Path("roomId") spaceId: String, + @Body params: SpaceSummaryParams + ): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt new file mode 100644 index 0000000000..5021ff638f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt @@ -0,0 +1,96 @@ +/* + * 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 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 room type, which is m.space for subspaces. + * It can be omitted if there is no room type in which case it should be interpreted as a normal room. + */ + @Json(name = "room_type") val roomType: String? = null, + + /** + * Aliases of the room. May be empty. + */ + @Json(name = "aliases") + val aliases: List? = null, + + /** + * The canonical alias of the room, if any. + */ + @Json(name = "canonical_alias") + val canonicalAlias: String? = null, + + /** + * The name of the room, if any. + */ + @Json(name = "name") + val name: String? = null, + + /** + * Required. The number of members joined to the room. + */ + @Json(name = "num_joined_members") + val numJoinedMembers: Int = 0, + + /** + * Required. The ID of the room. + */ + @Json(name = "room_id") + val roomId: String, + + /** + * The topic of the room, if any. + */ + @Json(name = "topic") + val topic: String? = null, + + /** + * Required. Whether the room may be viewed by guest users without joining. + */ + @Json(name = "world_readable") + val worldReadable: Boolean = false, + + /** + * Required. Whether guest users may join the room and participate in it. If they can, + * they will be subject to ordinary power level rules like any other user. + */ + @Json(name = "guest_can_join") + val guestCanJoin: Boolean = false, + + /** + * The URL for the room's avatar, if one is set. + */ + @Json(name = "avatar_url") + val avatarUrl: String? = null, + + /** + * Undocumented item + */ + @Json(name = "m.federate") + val isFederated: Boolean = false +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt new file mode 100644 index 0000000000..ba15f2e981 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt @@ -0,0 +1,48 @@ +/* + * 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 dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.space.peeking.DefaultPeekSpaceTask +import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask +import retrofit2.Retrofit + +@Module +internal abstract class SpaceModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesSpacesAPI(retrofit: Retrofit): SpaceApi { + return retrofit.create(SpaceApi::class.java) + } + } + + @Binds + abstract fun bindResolveSpaceTask(task: DefaultResolveSpaceInfoTask): ResolveSpaceInfoTask + + @Binds + abstract fun bindPeekSpaceTask(task: DefaultPeekSpaceTask): PeekSpaceTask + + @Binds + abstract fun bindJoinSpaceTask(task: DefaultJoinSpaceTask): JoinSpaceTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt new file mode 100644 index 0000000000..a3c6b3cc84 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt @@ -0,0 +1,30 @@ +/* + * 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 = 100, + /** The maximum number of rooms/subspaces to return, server can override this, default: 100 */ + @Json(name = "limit") val limit: Int = 100, + /** A token to use if this is a subsequent HTTP hit, default: "".*/ + @Json(name = "batch") val batch: String = "" +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpacesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpacesResponse.kt new file mode 100644 index 0000000000..20d63c8814 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpacesResponse.kt @@ -0,0 +1,31 @@ +/* + * 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 +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? = 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? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt index a2be75a232..1214befebd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 New Vector Ltd + * 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. diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt index 651411b2fe..eee8d1241f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt @@ -17,17 +17,14 @@ package im.vector.app.features.spaces.preview import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success import im.vector.app.R +import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.resources.StringProvider -import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericItemHeader import im.vector.app.core.utils.TextUtils import im.vector.app.features.home.AvatarRenderer -import org.matrix.android.sdk.api.session.room.peeking.PeekResult -import org.matrix.android.sdk.internal.session.space.peeking.ISpaceChild -import org.matrix.android.sdk.internal.session.space.peeking.SpaceChildPeekResult -import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult -import org.matrix.android.sdk.internal.session.space.peeking.SpaceSubChildPeekResult import javax.inject.Inject class SpacePreviewController @Inject constructor( @@ -40,78 +37,100 @@ class SpacePreviewController @Inject constructor( var interactionListener: InteractionListener? = null override fun buildModels(data: SpacePreviewState?) { - val result: SpacePeekResult = data?.peekResult?.invoke() ?: return + val result = data?.childInfoList?.invoke() ?: return - when (result) { - is SpacePeekResult.SpacePeekError -> { - genericFooterItem { - id("failed") - // TODO - text("Failed to resolve") - } + val memberCount = data.spaceInfo.invoke()?.memberCount ?: 0 + + spaceTopSummaryItem { + id("info") + formattedMemberCount(stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)) + topic(data.spaceInfo.invoke()?.topic ?: data.topic ?: "") + } + + if (result.isNotEmpty()) { + genericItemHeader { + id("header_rooms") + text(stringProvider.getString(R.string.rooms)) } - is SpacePeekResult.Success -> { - // add summary info - val memberCount = result.summary.roomPeekResult.numJoinedMembers ?: 0 - spaceTopSummaryItem { - id("info") - formattedMemberCount(stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)) - topic(result.summary.roomPeekResult.topic ?: "") - } - - genericItemHeader { - id("header_rooms") - text(stringProvider.getString(R.string.rooms)) - } - - buildChildren(result.summary.children, 0) - } + buildChildren(result, 0) } } - private fun buildChildren(children: List, depth: Int) { + private fun buildChildren(children: List, depth: Int) { children.forEach { child -> - when (child) { - is SpaceSubChildPeekResult -> { - when (val roomPeekResult = child.roomPeekResult) { - is PeekResult.Success -> { - subSpaceItem { - id(roomPeekResult.roomId) - roomId(roomPeekResult.roomId) - title(roomPeekResult.name) - depth(depth) - avatarUrl(roomPeekResult.avatarUrl) - avatarRenderer(avatarRenderer) - } - buildChildren(child.children, depth + 1) - } - else -> { - // ?? TODO - } + + if (child.isSubSpace == true) { + subSpaceItem { + id(child.roomId) + roomId(child.roomId) + title(child.name) + depth(depth) + avatarUrl(child.avatarUrl) + avatarRenderer(avatarRenderer) + } + when (child.children) { + is Loading -> { + loadingItem { id("loading_children_${child.roomId}") } + } + is Success -> { + buildChildren(child.children.invoke(), depth + 1) + } + else -> { } } - is SpaceChildPeekResult -> { - // We have to check if the peek result was success - when (val roomPeekResult = child.roomPeekResult) { - is PeekResult.Success -> { - roomChildItem { - id(child.id) - depth(depth) - roomId(roomPeekResult.roomId) - title(roomPeekResult.name ?: "") - topic(roomPeekResult.topic ?: "") - avatarUrl(roomPeekResult.avatarUrl) - memberCount(TextUtils.formatCountToShortDecimal(roomPeekResult.numJoinedMembers ?: 0)) - avatarRenderer(avatarRenderer) - } - } - else -> { - // What to do here? - } - } + } else { + roomChildItem { + id(child.roomId) + depth(depth) + roomId(child.roomId) + title(child.name ?: "") + topic(child.topic ?: "") + avatarUrl(child.avatarUrl) + memberCount(TextUtils.formatCountToShortDecimal(child.memberCount ?: 0)) + avatarRenderer(avatarRenderer) } } +// when (child) { +// is SpaceSubChildPeekResult -> { +// when (val roomPeekResult = child.roomPeekResult) { +// is PeekResult.Success -> { +// subSpaceItem { +// id(roomPeekResult.roomId) +// roomId(roomPeekResult.roomId) +// title(roomPeekResult.name) +// depth(depth) +// avatarUrl(roomPeekResult.avatarUrl) +// avatarRenderer(avatarRenderer) +// } +// buildChildren(child.children, depth + 1) +// } +// else -> { +// // ?? TODO +// } +// } +// } +// is SpaceChildPeekResult -> { +// // We have to check if the peek result was success +// when (val roomPeekResult = child.roomPeekResult) { +// is PeekResult.Success -> { +// roomChildItem { +// id(child.id) +// depth(depth) +// roomId(roomPeekResult.roomId) +// title(roomPeekResult.name ?: "") +// topic(roomPeekResult.topic ?: "") +// avatarUrl(roomPeekResult.avatarUrl) +// memberCount(TextUtils.formatCountToShortDecimal(roomPeekResult.numJoinedMembers ?: 0)) +// avatarRenderer(avatarRenderer) +// } +// } +// else -> { +// // What to do here? +// } +// } +// } +// } } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt index fcf961f23a..563b4f39e0 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt @@ -40,7 +40,6 @@ import im.vector.app.features.spaces.SpacePreviewSharedActionViewModel import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.util.MatrixItem -import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -99,7 +98,7 @@ class SpacePreviewFragment @Inject constructor( } override fun invalidate() = withState(viewModel) { - when (it.peekResult) { + when (it.spaceInfo) { is Uninitialized, is Loading -> { views.spacePreviewPeekingProgress.isVisible = true @@ -141,21 +140,23 @@ class SpacePreviewFragment @Inject constructor( } private fun updateToolbar(spacePreviewState: SpacePreviewState) { - when (val preview = spacePreviewState.peekResult.invoke()) { - is SpacePeekResult.Success -> { - val roomPeekResult = preview.summary.roomPeekResult - val mxItem = MatrixItem.RoomItem(roomPeekResult.roomId, roomPeekResult.name, roomPeekResult.avatarUrl) - avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar) - views.roomPreviewNoPreviewToolbarTitle.text = roomPeekResult.name - } - is SpacePeekResult.SpacePeekError, - null -> { - // what to do here? - val mxItem = MatrixItem.RoomItem(spacePreviewState.idOrAlias, spacePreviewState.name, spacePreviewState.avatarUrl) - avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar) - views.roomPreviewNoPreviewToolbarTitle.text = spacePreviewState.name - } - } +// when (val preview = spacePreviewState.peekResult.invoke()) { +// is SpacePeekResult.Success -> { +// val roomPeekResult = preview.summary.roomPeekResult + val spaceName = spacePreviewState.spaceInfo.invoke()?.name ?: spacePreviewState.name ?: "" + val spaceAvatarUrl = spacePreviewState.spaceInfo.invoke()?.avatarUrl ?: spacePreviewState.avatarUrl + val mxItem = MatrixItem.RoomItem(spacePreviewState.idOrAlias, spaceName, spaceAvatarUrl) + avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar) + views.roomPreviewNoPreviewToolbarTitle.text = spaceName +// } +// is SpacePeekResult.SpacePeekError, +// null -> { +// // what to do here? +// val mxItem = MatrixItem.RoomItem(spacePreviewState.idOrAlias, spacePreviewState.name, spacePreviewState.avatarUrl) +// avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar) +// views.roomPreviewNoPreviewToolbarTitle.text = spacePreviewState.name +// } +// } } override fun onStart() { diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt index 41d94e8c9d..cf64672046 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt @@ -19,13 +19,25 @@ package im.vector.app.features.spaces.preview import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized -import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult data class SpacePreviewState( val idOrAlias: String, val name: String? = null, + val topic: String? = null, val avatarUrl: String? = null, - val peekResult: Async = Uninitialized + val spaceInfo: Async = Uninitialized, + val childInfoList: Async> = Uninitialized ) : MvRxState { constructor(args: SpacePreviewArgs) : this(idOrAlias = args.idOrAlias) } + +data class ChildInfo( + val roomId: String, + val avatarUrl: String?, + val name: String?, + val topic: String?, + val memberCount: Int?, + val isSubSpace: Boolean?, + val viaServers: List?, + val children: Async> +) diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt index 6986db18aa..d11fe1502b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt @@ -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 com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -30,9 +31,12 @@ import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult +import org.matrix.android.sdk.internal.session.space.peeking.SpaceSubChildPeekResult +import timber.log.Timber class SpacePreviewViewModel @AssistedInject constructor( @Assisted private val initialState: SpacePreviewState, @@ -73,31 +77,29 @@ class SpacePreviewViewModel @AssistedInject constructor( } } - private fun handleDismissInvite() { - TODO("Not yet implemented") + private fun handleDismissInvite() = withState { state -> + // Here we need to join the space himself as well as the default rooms in that space + // TODO modal loading + viewModelScope.launch(Dispatchers.IO) { + try { + session.spaceService().rejectInvite(initialState.idOrAlias, null) + } catch (failure: Throwable) { + Timber.e(failure, "## Space: Failed to reject invite") + } + } } private fun handleAcceptInvite() = withState { state -> // Here we need to join the space himself as well as the default rooms in that space - val spaceInfo = state.peekResult.invoke() as? SpacePeekResult.Success - // TODO if we have no summary, we cannot find auto join rooms... // So maybe we should trigger a retry on summary after the join? - val spaceVia = (spaceInfo?.summary?.roomPeekResult as? PeekResult.Success)?.viaServers ?: emptyList() - val autoJoinChildren = spaceInfo?.summary?.children - ?.filter { it.default == true } - ?.map { - SpaceService.ChildAutoJoinInfo( - it.id, - // via servers - (it.roomPeekResult as? PeekResult.Success)?.viaServers ?: emptyList() - ) - } ?: emptyList() + val spaceInfo = state.spaceInfo.invoke() + val spaceVia = spaceInfo?.viaServers ?: emptyList() // trigger modal loading _viewEvents.post(SpacePreviewViewEvents.StartJoining) viewModelScope.launch(Dispatchers.IO) { - val joinResult = session.spaceService().joinSpace(spaceInfo?.summary?.idOrAlias ?: initialState.idOrAlias, null, spaceVia, autoJoinChildren) + val joinResult = session.spaceService().joinSpace(initialState.idOrAlias, null, spaceVia, emptyList()) when (joinResult) { SpaceService.JoinSpaceResult.Success, is SpaceService.JoinSpaceResult.PartialSuccess -> { @@ -116,20 +118,110 @@ class SpacePreviewViewModel @AssistedInject constructor( initialized = true // peek for the room setState { - copy(peekResult = Loading()) + copy( + spaceInfo = Loading(), + childInfoList = Loading() + ) } viewModelScope.launch(Dispatchers.IO) { try { - val result = session.spaceService().peekSpace(initialState.idOrAlias) - setState { - copy(peekResult = Success(result)) - } + resolveSpaceInfo() } catch (failure: Throwable) { - setState { - copy(peekResult = Fail(failure)) - } + Timber.e(failure, "## Space: Failed to resolve space info. Fallback to picking") + fallBackResolve() } } } } + + private suspend fun resolveSpaceInfo() { + val resolveResult = session.spaceService().querySpaceChildren(initialState.idOrAlias) + setState { + copy( + spaceInfo = Success( + resolveResult.first.let { + ChildInfo( + roomId = it.roomId, + avatarUrl = it.avatarUrl, + name = it.name, + topic = it.topic, + memberCount = it.joinedMembersCount, + isSubSpace = it.roomType == RoomType.SPACE, + children = Uninitialized, + viaServers = null + ) + } + ), + childInfoList = Success( + resolveResult.second.map { + ChildInfo( + roomId = it.roomSummary?.roomId ?: "", + avatarUrl = it.roomSummary?.avatarUrl, + name = it.roomSummary?.name, + topic = it.roomSummary?.topic, + memberCount = it.roomSummary?.joinedMembersCount, + isSubSpace = it.roomSummary?.roomType == RoomType.SPACE, + children = Uninitialized, + viaServers = null + ) + } + ) + ) + } + } + + private suspend fun fallBackResolve() { + try { + val resolveResult: SpacePeekResult = session.spaceService().peekSpace(initialState.idOrAlias) + val spaceInfo = (resolveResult as? SpacePeekResult.Success)?.summary?.roomPeekResult + setState { + copy( + spaceInfo = Success( + ChildInfo( + roomId = spaceInfo?.roomId ?: initialState.idOrAlias, + avatarUrl = spaceInfo?.avatarUrl, + name = spaceInfo?.name, + topic = spaceInfo?.topic, + memberCount = spaceInfo?.numJoinedMembers, + isSubSpace = true, + children = Uninitialized, + viaServers = spaceInfo?.viaServers + + ) + ), + childInfoList = resolveResult.let { + when (it) { + is SpacePeekResult.Success -> { + (resolveResult as SpacePeekResult.Success).summary.children.mapNotNull { spaceChild -> + val roomPeekResult = spaceChild.roomPeekResult + if (roomPeekResult is PeekResult.Success) { + ChildInfo( + roomId = spaceChild.id, + avatarUrl = roomPeekResult.avatarUrl, + name = roomPeekResult.name, + topic = roomPeekResult.topic, + memberCount = roomPeekResult.numJoinedMembers, + isSubSpace = spaceChild is SpaceSubChildPeekResult, + children = Uninitialized, + viaServers = roomPeekResult.viaServers + + ) + } else { + null + } + } + Success(emptyList()) + } + else -> { + Fail(Exception("Failed to get info")) + } + } + }) + } + } catch (failure: Throwable) { + setState { + copy(spaceInfo = Fail(failure), childInfoList = Fail(failure)) + } + } + } }