diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 9d8f18e912..4be8eea856 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -52,6 +52,7 @@ object EventType { const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access" const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels" const val STATE_SPACE_CHILD = "m.space.child" +// const val STATE_SPACE_CHILD = "org.matrix.msc1772.space" /** * Note that this Event has been deprecated, see diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt index db70dadef3..a27e88aced 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt @@ -34,4 +34,6 @@ sealed class PeekResult { ) : PeekResult() object UnknownAlias : PeekResult() + + fun isSuccess() = this is Success } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt index 75480282fa..88ac00cc55 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt @@ -23,4 +23,6 @@ interface Space { fun asRoom() : Room suspend fun addRoom(roomId: String) + +// fun getChildren() : List } 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 0c3461f1ca..4043a3f7b4 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,7 @@ 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.internal.session.space.peeking.SpacePeekResult typealias SpaceSummaryQueryParams = RoomSummaryQueryParams @@ -35,6 +36,13 @@ interface SpaceService { */ fun getSpace(spaceId: String): Space? + /** + * Try to resolve (peek) rooms and subspace in this space. + * Use this call get preview of children of this space, particularly useful to get a + * preview of rooms that you did not join yet. + */ + suspend fun peekSpace(spaceId: String) : SpacePeekResult + /** * Get a live list of space summaries. This list is refreshed as soon as the data changes. * @return the [LiveData] of List[SpaceSummary] @@ -42,4 +50,23 @@ interface SpaceService { fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List + + data class ChildAutoJoinInfo( + val roomIdOrAlias: String, + val viaServers: List = emptyList() + ) + + sealed class JoinSpaceResult { + object Success: JoinSpaceResult() + data class Fail(val error: Throwable?): JoinSpaceResult() + /** Success fully joined the space, but failed to join all or some of it's rooms */ + data class PartialSuccess(val failedRooms: Map) : JoinSpaceResult() + + fun isSuccess() = this is Success || this is PartialSuccess + } + + suspend fun joinSpace(spaceIdOrAlias: String, + reason: String? = null, + viaServers: List = emptyList(), + autoJoinChild: List) : JoinSpaceResult } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt index f65318b543..f7bd067c55 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt @@ -35,6 +35,7 @@ data class SpaceChildContent( @Json(name = "via") val via: List? = null, /** * present: true key is included to distinguish from a deleted state event + * Children where present is not present or is not set to true are ignored. */ @Json(name = "present") val present: Boolean? = false, /** 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 8f3445bec3..b7c4246eca 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 @@ -91,6 +91,8 @@ 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.DefaultSpaceService +import org.matrix.android.sdk.internal.session.space.peeking.DefaultPeekSpaceTask +import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask import retrofit2.Retrofit @Module @@ -236,6 +238,9 @@ internal abstract class RoomModule { @Binds abstract fun bindPeekRoomTask(task: DefaultPeekRoomTask): PeekRoomTask + @Binds + abstract fun bindPeekSpaceTask(task: DefaultPeekSpaceTask): PeekSpaceTask + @Binds abstract fun bindGetEventTask(task: DefaultGetEventTask): GetEventTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomRelationshipHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomRelationshipHelper.kt index 4025861caa..b1bcfc7077 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomRelationshipHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomRelationshipHelper.kt @@ -44,7 +44,7 @@ internal class RoomRelationshipHelper(private val realm: Realm, .filter { ContentMapper.map(it.root?.content).toModel()?.present == true } .mapNotNull { // ContentMapper.map(it.root?.content).toModel() - it.roomId + it.stateKey } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index f8a3495aa2..7b637cc9e9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -167,7 +167,7 @@ internal class RoomSummaryUpdater @Inject constructor( spaceSummaryEntity.children.clear() spaceSummaryEntity.children.addAll( RoomRelationshipHelper(realm, roomId).getDirectChildrenDescriptions() - .map { RoomSummaryEntity.getOrCreate(realm, roomId) } + .map { RoomSummaryEntity.getOrCreate(realm, it) } ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt index ae71ee5cf2..ebe845572d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt @@ -35,4 +35,12 @@ class DefaultSpace(private val room: Room) : Space { body = SpaceChildContent(present = true).toContent() ) } + +// override fun getChildren(): List { +// // asRoom().getStateEvents(setOf(EventType.STATE_SPACE_CHILD)).mapNotNull { +// // // statekeys are the roomIds +// // +// // } +// return emptyList() +// } } 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 3d9e7d7764..4118d74604 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 @@ -32,6 +32,8 @@ 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.read.MarkAllRoomsReadTask +import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask +import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask import org.matrix.android.sdk.internal.task.TaskExecutor import javax.inject.Inject @@ -46,6 +48,7 @@ internal class DefaultSpaceService @Inject constructor( private val deleteRoomAliasTask: DeleteRoomAliasTask, private val roomGetter: RoomGetter, private val spaceSummaryDataSource: SpaceSummaryDataSource, + private val peekSpaceTask: PeekSpaceTask, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, private val taskExecutor: TaskExecutor ) : SpaceService { @@ -67,4 +70,31 @@ internal class DefaultSpaceService @Inject constructor( override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List { return spaceSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams) } + + override suspend fun peekSpace(spaceId: String): SpacePeekResult { + return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId)) + } + + override suspend fun joinSpace(spaceIdOrAlias: String, reason: String?, viaServers: List, autoJoinChild: List): SpaceService.JoinSpaceResult { + try { + joinRoomTask.execute(JoinRoomTask.Params(spaceIdOrAlias, reason, viaServers)) + val childJoinFailures = mutableMapOf() + autoJoinChild.forEach { info -> + // TODO what if the child is it self a subspace with some default children? + try { + joinRoomTask.execute(JoinRoomTask.Params(info.roomIdOrAlias, null, info.viaServers)) + } catch (failure: Throwable) { + // TODO, i could already be a member of this room, handle that as it should not be an error in this context + childJoinFailures[info.roomIdOrAlias] = failure + } + } + return if (childJoinFailures.isEmpty()) { + SpaceService.JoinSpaceResult.Success + } else { + SpaceService.JoinSpaceResult.PartialSuccess(childJoinFailures) + } + } catch (throwable: Throwable) { + return SpaceService.JoinSpaceResult.Fail(throwable) + } + } } 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 new file mode 100644 index 0000000000..826be0b3aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2020 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.peeking + +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.toModel +import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.api.session.room.peeking.PeekResult +import org.matrix.android.sdk.api.session.space.model.SpaceChildContent +import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask +import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask +import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber +import javax.inject.Inject + +internal interface PeekSpaceTask : Task { + data class Params( + val roomIdOrAlias: String, + // A depth limit as a simple protection against cycles + val maxDepth: Int = 4 + ) +} + +internal class DefaultPeekSpaceTask @Inject constructor( + private val peekRoomTask: PeekRoomTask, + private val resolveRoomStateTask: ResolveRoomStateTask +) : PeekSpaceTask { + + override suspend fun execute(params: PeekSpaceTask.Params): SpacePeekResult { + val peekResult = peekRoomTask.execute(PeekRoomTask.Params(params.roomIdOrAlias)) + val roomResult = peekResult as? PeekResult.Success ?: return SpacePeekResult.FailedToResolve(params.roomIdOrAlias, peekResult) + + // check the room type + // kind of duplicate cause we already did it in Peek? could we pass on the result?? + val stateEvents = try { + resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomResult.roomId)) + } catch (failure: Throwable) { + return SpacePeekResult.FailedToResolve(params.roomIdOrAlias, peekResult) + } + val isSpace = stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_CREATE && it.stateKey == "" } + ?.let { it.content?.toModel()?.type } == RoomType.SPACE + + if (!isSpace) return SpacePeekResult.NotSpaceType(params.roomIdOrAlias) + + val children = peekChildren(stateEvents, 0, params.maxDepth) + + return SpacePeekResult.Success( + SpacePeekSummary( + params.roomIdOrAlias, + peekResult, + children + ) + ) + } + + private suspend fun peekChildren(stateEvents: List, depth: Int, maxDepth: Int): List { + if (depth >= maxDepth) return emptyList() + val childRoomsIds = stateEvents + .filter { + it.type == EventType.STATE_SPACE_CHILD && !it.stateKey.isNullOrEmpty() + // Children where present is not present or is not set to true are ignored. + && it.content?.toModel()?.present == true + } + .map { it.stateKey to it.content?.toModel() } + + Timber.v("## SPACE_PEEK: found ${childRoomsIds.size} present children") + + val spaceChildResults = mutableListOf() + childRoomsIds.forEach { entry -> + + Timber.v("## SPACE_PEEK: peeking child $entry") + // peek each child + val childId = entry.first ?: return@forEach + try { + val childPeek = peekRoomTask.execute(PeekRoomTask.Params(childId)) + + val childStateEvents = resolveRoomStateTask.execute(ResolveRoomStateTask.Params(childId)) + val createContent = childStateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_CREATE && it.stateKey == "" } + ?.let { it.content?.toModel() } + + if (!childPeek.isSuccess() || createContent == null) { + Timber.v("## SPACE_PEEK: cannot peek child $entry") + // can't peek :/ + spaceChildResults.add( + SpaceChildPeekResult( + childId, childPeek, entry.second?.default, entry.second?.order + ) + ) + // continue to next child + return@forEach + } + val type = createContent.type + if (type == RoomType.SPACE) { + Timber.v("## SPACE_PEEK: subspace child $entry") + spaceChildResults.add( + SpaceSubChildPeekResult( + childId, + childPeek, + entry.second?.default, + entry.second?.order, + peekChildren(childStateEvents, depth + 1, maxDepth) + ) + ) + } else if (type == RoomType.MESSAGING || type == null) { + Timber.v("## SPACE_PEEK: room child $entry") + spaceChildResults.add( + SpaceChildPeekResult( + childId, childPeek, entry.second?.default, entry.second?.order + ) + ) + } else { + // ignore for now? + } + + // let's check child info + } catch (failure: Throwable) { + // can this happen? + Timber.e(failure, "## Failed to resolve space child") + } + } + return spaceChildResults + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt new file mode 100644 index 0000000000..63eed2a6c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 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.peeking + +import org.matrix.android.sdk.api.session.room.peeking.PeekResult + +data class SpacePeekSummary( + val idOrAlias: String, + val roomPeekResult: PeekResult.Success, + val children: List +) + +interface ISpaceChild { + val id: String + val roomPeekResult: PeekResult + val default: Boolean? + val order: String? +} + +data class SpaceChildPeekResult( + override val id: String, + override val roomPeekResult: PeekResult, + 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 order: String?, + val children: List +) : ISpaceChild + +sealed class SpacePeekResult { + abstract class SpacePeekError : SpacePeekResult() + data class FailedToResolve(val spaceId: String, val roomPeekResult: PeekResult) : SpacePeekError() + data class NotSpaceType(val spaceId: String) : SpacePeekError() + + data class Success(val summary: SpacePeekSummary): SpacePeekResult() +} diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 07606d315c..9a1f2e6dfd 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -271,7 +271,8 @@ - + + ", R.string.command_confetti), SNOW("/snow", "", R.string.command_snow), - CREATE_SPACE("/createspace", " *", R.string.command_description_create_space); + CREATE_SPACE("/createspace", " *", R.string.command_description_create_space), + ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space); val length get() = command.length + 1 diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 57466ddf98..fe5707ec45 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -296,7 +296,7 @@ object CommandParser { val message = textMessage.substring(Command.CONFETTI.command.length).trim() ParsedCommand.SendChatEffect(ChatEffect.CONFETTI, message) } - Command.SNOW.command -> { + Command.SNOW.command -> { val message = textMessage.substring(Command.SNOW.command.length).trim() ParsedCommand.SendChatEffect(ChatEffect.SNOW, message) } @@ -312,6 +312,12 @@ object CommandParser { ) } } + Command.ADD_TO_SPACE.command -> { + val rawCommand = textMessage.substring(Command.ADD_TO_SPACE.command.length).trim() + ParsedCommand.AddToSpace( + rawCommand + ) + } else -> { // Unknown command ParsedCommand.ErrorUnknownSlashCommand(slashCommand) diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index 1017b29234..99b0ae7889 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -58,4 +58,5 @@ sealed class ParsedCommand { object DiscardSession : ParsedCommand() class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand() class CreateSpace(val name: String, val invitees: List) : ParsedCommand() + class AddToSpace(val spaceId: String) : ParsedCommand() } diff --git a/vector/src/main/java/im/vector/app/features/grouplist/SpaceListFragment.kt b/vector/src/main/java/im/vector/app/features/grouplist/SpaceListFragment.kt index 4c783cb2d4..7091c0b86c 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/SpaceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/SpaceListFragment.kt @@ -59,7 +59,8 @@ class SpaceListFragment @Inject constructor( views.groupListView.configureWith(spaceController) viewModel.observeViewEvents { when (it) { - is SpaceListViewEvents.OpenSpaceSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup) + is SpaceListViewEvents.OpenSpaceSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenSpacePreview(it.id)) + is SpaceListViewEvents.OpenSpace -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup) }.exhaustive } } diff --git a/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryController.kt b/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryController.kt index c7a27266fd..dab2cbceae 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryController.kt @@ -18,9 +18,13 @@ package im.vector.app.features.grouplist import com.airbnb.epoxy.EpoxyController +import im.vector.app.R 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.features.home.AvatarRenderer import im.vector.app.features.spaces.SpaceListViewState +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -50,24 +54,57 @@ class SpaceSummaryController @Inject constructor( if (summaries.isNullOrEmpty()) { return } - summaries.forEach { groupSummary -> - val isSelected = groupSummary.spaceId == selected?.spaceId - if (groupSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) { - homeSpaceSummaryItem { - id(groupSummary.spaceId) - selected(isSelected) - listener { callback?.onSpaceSelected(groupSummary) } + // show invites on top + + summaries.filter { it.roomSummary.membership == Membership.INVITE } + .let { invites -> + if (invites.isNotEmpty()) { + genericItemHeader { + id("invites") + text(stringProvider.getString(R.string.spaces_invited_header)) + } + invites.forEach { + spaceSummaryItem { + avatarRenderer(avatarRenderer) + id(it.spaceId) + matrixItem(it.toMatrixItem()) + selected(false) + listener { callback?.onSpaceSelected(it) } + } + } + genericFooterItem { + id("invite_space") + text("") + } + } } - } else { - spaceSummaryItem { - avatarRenderer(avatarRenderer) - id(groupSummary.spaceId) - matrixItem(groupSummary.toMatrixItem()) - selected(isSelected) - listener { callback?.onSpaceSelected(groupSummary) } - } - } + + genericItemHeader { + id("spaces") + text(stringProvider.getString(R.string.spaces_header)) } + + summaries + .filter { it.roomSummary.membership == Membership.JOIN } + .forEach { groupSummary -> + + val isSelected = groupSummary.spaceId == selected?.spaceId + if (groupSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) { + homeSpaceSummaryItem { + id(groupSummary.spaceId) + selected(isSelected) + listener { callback?.onSpaceSelected(groupSummary) } + } + } else { + spaceSummaryItem { + avatarRenderer(avatarRenderer) + id(groupSummary.spaceId) + matrixItem(groupSummary.toMatrixItem()) + selected(isSelected) + listener { callback?.onSpaceSelected(groupSummary) } + } + } + } } interface Callback { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 60a8836be5..138ffc26f4 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -54,6 +54,7 @@ import im.vector.app.features.popup.VerificationVectorAlert import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity +import im.vector.app.features.spaces.SpacePreviewActivity import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import im.vector.app.features.workers.signout.ServerBackupStatusViewState @@ -141,6 +142,9 @@ class HomeActivity : views.drawerLayout.closeDrawer(GravityCompat.START) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true) } + is HomeActivitySharedAction.OpenSpacePreview -> { + startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId)) + } }.exhaustive } .disposeOnDestroy() diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt index 52b3c58785..f72354465b 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt @@ -25,4 +25,5 @@ sealed class HomeActivitySharedAction : VectorSharedAction { object OpenDrawer : HomeActivitySharedAction() object CloseDrawer : HomeActivitySharedAction() object OpenGroup : HomeActivitySharedAction() + data class OpenSpacePreview(val spaceId: String) : HomeActivitySharedAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index bdf38719e2..785449236c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -839,6 +839,17 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) popDraft() } + is ParsedCommand.AddToSpace -> { + viewModelScope.launch(Dispatchers.IO) { + try { + session.spaceService().getSpace(slashCommandResult.spaceId)?.addRoom(room.roomId) + } catch (failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) + } + } + _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) + popDraft() + } }.exhaustive } is SendMode.EDIT -> { diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt new file mode 100644 index 0000000000..dacde8846c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt @@ -0,0 +1,72 @@ +/* + * 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 im.vector.app.features.spaces + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.airbnb.mvrx.MvRx +import im.vector.app.R +import im.vector.app.core.extensions.commitTransaction +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivitySimpleBinding +import im.vector.app.features.spaces.preview.SpacePreviewArgs +import im.vector.app.features.spaces.preview.SpacePreviewFragment + +class SpacePreviewActivity : VectorBaseActivity() { + + lateinit var sharedActionViewModel: SpacePreviewSharedActionViewModel + + override fun getBinding(): ActivitySimpleBinding = ActivitySimpleBinding.inflate(layoutInflater) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedActionViewModel = viewModelProvider.get(SpacePreviewSharedActionViewModel::class.java) + sharedActionViewModel + .observe() + .subscribe { action -> + when (action) { + SpacePreviewSharedAction.DismissAction -> finish() + SpacePreviewSharedAction.ShowModalLoading -> showWaitingView() + SpacePreviewSharedAction.HideModalLoading -> hideWaitingView() + is SpacePreviewSharedAction.ShowErrorMessage -> action.error?.let { showSnackbar(it) } + } + }.disposeOnDestroy() + + if (isFirstCreation()) { + val simpleName = SpacePreviewFragment::class.java.simpleName + val args = intent?.getParcelableExtra(MvRx.KEY_ARG) + if (supportFragmentManager.findFragmentByTag(simpleName) == null) { + supportFragmentManager.commitTransaction { + replace(R.id.simpleFragmentContainer, + SpacePreviewFragment::class.java, + Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) }, + simpleName + ) + } + } + } + } + + companion object { + fun newIntent(context: Context, spaceIdOrAlias: String): Intent { + return Intent(context, SpacePreviewActivity::class.java).apply { + putExtra(MvRx.KEY_ARG, SpacePreviewArgs(spaceIdOrAlias)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewSharedActionViewModel.kt new file mode 100644 index 0000000000..058b1a275b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewSharedActionViewModel.kt @@ -0,0 +1,30 @@ +/* + * 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 im.vector.app.features.spaces + +import im.vector.app.core.platform.VectorSharedAction +import im.vector.app.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +sealed class SpacePreviewSharedAction : VectorSharedAction { + object DismissAction : SpacePreviewSharedAction() + object ShowModalLoading : SpacePreviewSharedAction() + object HideModalLoading : SpacePreviewSharedAction() + data class ShowErrorMessage(val error: String? = null) : SpacePreviewSharedAction() +} + +class SpacePreviewSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt index 5daedcc984..f0d8ae30f7 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt @@ -53,7 +53,8 @@ sealed class SpaceListAction : VectorViewModelAction { * Transient events for group list screen */ sealed class SpaceListViewEvents : VectorViewEvents { - object OpenSpaceSummary : SpaceListViewEvents() + object OpenSpace : SpaceListViewEvents() + data class OpenSpaceSummary(val id: String) : SpaceListViewEvents() } data class SpaceListViewState( @@ -94,7 +95,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp // We only want to open group if the updated selectedGroup is a different one. if (currentGroupId != spaceSummary.spaceId) { currentGroupId = spaceSummary.spaceId - _viewEvents.post(SpaceListViewEvents.OpenSpaceSummary) + _viewEvents.post(SpaceListViewEvents.OpenSpace) } val optionGroup = Option.just(spaceSummary) selectedSpaceDataSource.post(optionGroup) @@ -116,20 +117,27 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp // PRIVATE METHODS ***************************************************************************** private fun handleSelectSpace(action: SpaceListAction.SelectSpace) = withState { state -> - if (state.selectedSpace?.spaceId != action.spaceSummary.spaceId) { - // We take care of refreshing group data when selecting to be sure we get all the rooms and users -// tryOrNull { -// viewModelScope.launch { -// session.getGroup(action.spaceSummary.groupId)?.fetchGroupData() + + if (state.selectedSpace?.roomSummary?.membership == Membership.INVITE) { + _viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(state.selectedSpace.roomSummary.roomId)) +// viewModelScope.launch(Dispatchers.IO) { +// tryOrNull { session.spaceService().peekSpace(action.spaceSummary.spaceId) }.let { +// Timber.d("PEEK RESULT/ $it") // } // } - setState { copy(selectedSpace = action.spaceSummary) } + } else { + if (state.selectedSpace?.spaceId != action.spaceSummary.spaceId) { +// state.selectedSpace?.let { +// selectedSpaceDataSource.post(Option.just(state.selectedSpace)) +// } + setState { copy(selectedSpace = action.spaceSummary) } + } } } private fun observeGroupSummaries() { val roomSummaryQueryParams = roomSummaryQueryParams() { - memberships = listOf(Membership.JOIN) + memberships = listOf(Membership.JOIN, Membership.INVITE) displayName = QueryStringValue.IsNotEmpty excludeType = listOf(RoomType.MESSAGING, null) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/RoomChildItem.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/RoomChildItem.kt new file mode 100644 index 0000000000..bf28618c6c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/RoomChildItem.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2020 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 im.vector.app.features.spaces.preview + +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.MatrixItem + +@EpoxyModelClass(layout = R.layout.item_space_roomchild) +abstract class RoomChildItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var roomId: String + + @EpoxyAttribute + lateinit var title: String + + @EpoxyAttribute + var topic: String? = null + + @EpoxyAttribute + lateinit var memberCount: String + + @EpoxyAttribute + var avatarUrl: String? = null + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + var depth: Int = 0 + + override fun bind(holder: Holder) { + super.bind(holder) + holder.roomNameText.text = title + holder.roomTopicText.setTextOrHide(topic) + holder.memberCountText.text = memberCount + + avatarRenderer.render( + MatrixItem.RoomItem(roomId, title, avatarUrl), + holder.avatarImageView + ) + holder.tabView.tabDepth = depth + } + + override fun unbind(holder: Holder) { + avatarRenderer.clear(holder.avatarImageView) + super.unbind(holder) + } + + class Holder : VectorEpoxyHolder() { + val avatarImageView by bind(R.id.childRoomAvatar) + val roomNameText by bind(R.id.childRoomName) + val roomTopicText by bind(R.id.childRoomTopic) + val memberCountText by bind(R.id.spaceChildMemberCountText) + val tabView by bind(R.id.spaceChildTabView) + } +} 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 new file mode 100644 index 0000000000..651411b2fe --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2020 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 im.vector.app.features.spaces.preview + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R +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( + private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider +) : TypedEpoxyController() { + + interface InteractionListener + + var interactionListener: InteractionListener? = null + + override fun buildModels(data: SpacePreviewState?) { + val result: SpacePeekResult = data?.peekResult?.invoke() ?: return + + when (result) { + is SpacePeekResult.SpacePeekError -> { + genericFooterItem { + id("failed") + // TODO + text("Failed to resolve") + } + } + 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) + } + } + } + + 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 + } + } + } + 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 new file mode 100644 index 0000000000..fcf961f23a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2020 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 im.vector.app.features.spaces.preview + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.appcompat.navigationClicks +import im.vector.app.R +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentSpacePreviewBinding +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.spaces.SpacePreviewSharedAction +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 + +@Parcelize +data class SpacePreviewArgs( + val idOrAlias: String +) : Parcelable + +class SpacePreviewFragment @Inject constructor( + private val viewModelFactory: SpacePreviewViewModel.Factory, + private val avatarRenderer: AvatarRenderer, + private val epoxyController: SpacePreviewController +) : VectorBaseFragment(), SpacePreviewViewModel.Factory { + + private val viewModel by fragmentViewModel(SpacePreviewViewModel::class) + lateinit var sharedActionViewModel: SpacePreviewSharedActionViewModel + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSpacePreviewBinding { + return FragmentSpacePreviewBinding.inflate(inflater, container, false) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedActionViewModel = activityViewModelProvider.get(SpacePreviewSharedActionViewModel::class.java) + } + + override fun create(initialState: SpacePreviewState) = viewModelFactory.create(initialState) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.observeViewEvents { + handleViewEvents(it) + } + + views.roomPreviewNoPreviewToolbar.navigationClicks() + .throttleFirst(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { sharedActionViewModel.post(SpacePreviewSharedAction.DismissAction) } + .disposeOnDestroyView() + + views.spacePreviewRecyclerView.configureWith(epoxyController) + + views.spacePreviewAcceptInviteButton.debouncedClicks { + viewModel.handle(SpacePreviewViewAction.AcceptInvite) + } + + views.spacePreviewDeclineInviteButton.debouncedClicks { + viewModel.handle(SpacePreviewViewAction.DismissInvite) + } + } + + override fun onDestroyView() { + views.spacePreviewRecyclerView.cleanup() + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { + when (it.peekResult) { + is Uninitialized, + is Loading -> { + views.spacePreviewPeekingProgress.isVisible = true + views.spacePreviewButtonBar.isVisible = true + views.spacePreviewAcceptInviteButton.isEnabled = false + views.spacePreviewDeclineInviteButton.isEnabled = false + } + is Fail -> { + views.spacePreviewPeekingProgress.isVisible = false + views.spacePreviewButtonBar.isVisible = false + } + is Success -> { + views.spacePreviewPeekingProgress.isVisible = false + views.spacePreviewButtonBar.isVisible = true + views.spacePreviewAcceptInviteButton.isEnabled = true + views.spacePreviewDeclineInviteButton.isEnabled = true + epoxyController.setData(it) + } + } + updateToolbar(it) + } + + private fun handleViewEvents(viewEvents: SpacePreviewViewEvents) { + when (viewEvents) { + SpacePreviewViewEvents.Dismiss -> { + } + SpacePreviewViewEvents.StartJoining -> { + sharedActionViewModel.post(SpacePreviewSharedAction.ShowModalLoading) + } + SpacePreviewViewEvents.JoinSuccess -> { + sharedActionViewModel.post(SpacePreviewSharedAction.HideModalLoading) + sharedActionViewModel.post(SpacePreviewSharedAction.DismissAction) + } + is SpacePreviewViewEvents.JoinFailure -> { + sharedActionViewModel.post(SpacePreviewSharedAction.HideModalLoading) + sharedActionViewModel.post(SpacePreviewSharedAction.ShowErrorMessage(viewEvents.message ?: getString(R.string.matrix_error))) + } + } + } + + 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 + } + } + } + + override fun onStart() { + super.onStart() + viewModel.handle(SpacePreviewViewAction.ViewReady) + } +} 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 new file mode 100644 index 0000000000..41d94e8c9d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt @@ -0,0 +1,31 @@ +/* + * 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 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 avatarUrl: String? = null, + val peekResult: Async = Uninitialized +) : MvRxState { + constructor(args: SpacePreviewArgs) : this(idOrAlias = args.idOrAlias) +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewAction.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewAction.kt new file mode 100644 index 0000000000..6426b89d55 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewAction.kt @@ -0,0 +1,25 @@ +/* + * 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 im.vector.app.features.spaces.preview + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class SpacePreviewViewAction : VectorViewModelAction { + object ViewReady : SpacePreviewViewAction() + object AcceptInvite : SpacePreviewViewAction() + object DismissInvite : SpacePreviewViewAction() +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt new file mode 100644 index 0000000000..04645e59ad --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt @@ -0,0 +1,26 @@ +/* + * 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 im.vector.app.features.spaces.preview + +import im.vector.app.core.platform.VectorViewEvents + +sealed class SpacePreviewViewEvents : VectorViewEvents { + object Dismiss: SpacePreviewViewEvents() + object StartJoining: SpacePreviewViewEvents() + object JoinSuccess: SpacePreviewViewEvents() + data class JoinFailure(val message: String?): SpacePreviewViewEvents() +} 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 new file mode 100644 index 0000000000..6986db18aa --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2020 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 im.vector.app.features.spaces.preview + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +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.peeking.PeekResult +import org.matrix.android.sdk.api.session.space.SpaceService +import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult + +class SpacePreviewViewModel @AssistedInject constructor( + @Assisted private val initialState: SpacePreviewState, + private val session: Session +) : VectorViewModel(initialState) { + + private var initialized = false + + init { + // do we have some things in cache? + session.getRoomSummary(initialState.idOrAlias)?.let { + setState { + copy(name = it.name, avatarUrl = it.avatarUrl) + } + } + } + + @AssistedInject.Factory + interface Factory { + fun create(initialState: SpacePreviewState): SpacePreviewViewModel + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: SpacePreviewState): SpacePreviewViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + override fun handle(action: SpacePreviewViewAction) { + when (action) { + SpacePreviewViewAction.ViewReady -> handleReady() + SpacePreviewViewAction.AcceptInvite -> handleAcceptInvite() + SpacePreviewViewAction.DismissInvite -> handleDismissInvite() + } + } + + private fun handleDismissInvite() { + TODO("Not yet implemented") + } + + 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() + + // trigger modal loading + _viewEvents.post(SpacePreviewViewEvents.StartJoining) + viewModelScope.launch(Dispatchers.IO) { + val joinResult = session.spaceService().joinSpace(spaceInfo?.summary?.idOrAlias ?: initialState.idOrAlias, null, spaceVia, autoJoinChildren) + when (joinResult) { + SpaceService.JoinSpaceResult.Success, + is SpaceService.JoinSpaceResult.PartialSuccess -> { + // For now we don't handle partial success, it's just success + _viewEvents.post(SpacePreviewViewEvents.JoinSuccess) + } + is SpaceService.JoinSpaceResult.Fail -> { + _viewEvents.post(SpacePreviewViewEvents.JoinFailure(joinResult.error?.toString())) + } + } + } + } + + private fun handleReady() { + if (!initialized) { + initialized = true + // peek for the room + setState { + copy(peekResult = Loading()) + } + viewModelScope.launch(Dispatchers.IO) { + try { + val result = session.spaceService().peekSpace(initialState.idOrAlias) + setState { + copy(peekResult = Success(result)) + } + } catch (failure: Throwable) { + setState { + copy(peekResult = Fail(failure)) + } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTabView.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTabView.kt new file mode 100644 index 0000000000..675e7070e7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTabView.kt @@ -0,0 +1,51 @@ +/* + * 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 im.vector.app.features.spaces.preview + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import im.vector.app.R + +class SpaceTabView constructor(context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0) + : LinearLayout(context, attrs, defStyleAttr) { + + constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) {} + constructor(context: Context) : this(context, null, 0) {} + + var tabDepth = 0 + set(value) { + if (field != value) { + field = value + setUpView() + } + } + + init { + setUpView() + } + + private fun setUpView() { + // remove children + removeAllViews() + for (i in 0 until tabDepth) { + inflate(context, R.layout.item_space_tab, this) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTopSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTopSummaryItem.kt new file mode 100644 index 0000000000..c357fb14b3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTopSummaryItem.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 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 im.vector.app.features.spaces.preview + +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_space_top_summary) +abstract class SpaceTopSummaryItem : VectorEpoxyModel() { + + @EpoxyAttribute + var topic: String? = null + + @EpoxyAttribute + lateinit var formattedMemberCount: String + + override fun bind(holder: Holder) { + super.bind(holder) + holder.spaceTopicText.setTextOrHide(topic) + holder.memberCountText.text = formattedMemberCount + } + + class Holder : VectorEpoxyHolder() { + val memberCountText by bind(R.id.spaceSummaryMemberCountText) + val spaceTopicText by bind(R.id.spaceSummaryTopic) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt new file mode 100644 index 0000000000..367a81fe5a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 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 im.vector.app.features.spaces.preview + +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.MatrixItem + +@EpoxyModelClass(layout = R.layout.item_space_subspace) +abstract class SubSpaceItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var roomId: String + + @EpoxyAttribute + lateinit var title: String + + @EpoxyAttribute + var avatarUrl: String? = null + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + var depth: Int = 0 + + override fun bind(holder: Holder) { + super.bind(holder) + holder.nameText.text = title + + avatarRenderer.renderSpace( + MatrixItem.RoomItem(roomId, title, avatarUrl), + holder.avatarImageView + ) + holder.tabView.tabDepth = depth + } + + override fun unbind(holder: Holder) { + avatarRenderer.clear(holder.avatarImageView) + super.unbind(holder) + } + + class Holder : VectorEpoxyHolder() { + val avatarImageView by bind(R.id.childSpaceAvatar) + val nameText by bind(R.id.childSpaceName) + val tabView by bind(R.id.childSpaceTab) + } +} diff --git a/vector/src/main/res/layout/fragment_space_preview.xml b/vector/src/main/res/layout/fragment_space_preview.xml new file mode 100644 index 0000000000..257797548e --- /dev/null +++ b/vector/src/main/res/layout/fragment_space_preview.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_space_roomchild.xml b/vector/src/main/res/layout/item_space_roomchild.xml new file mode 100644 index 0000000000..0fdbd833f4 --- /dev/null +++ b/vector/src/main/res/layout/item_space_roomchild.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_space_subspace.xml b/vector/src/main/res/layout/item_space_subspace.xml new file mode 100644 index 0000000000..ac654dc2b3 --- /dev/null +++ b/vector/src/main/res/layout/item_space_subspace.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_space_tab.xml b/vector/src/main/res/layout/item_space_tab.xml new file mode 100644 index 0000000000..ea08fabea3 --- /dev/null +++ b/vector/src/main/res/layout/item_space_tab.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/vector/src/main/res/layout/item_space_top_summary.xml b/vector/src/main/res/layout/item_space_top_summary.xml new file mode 100644 index 0000000000..e4e2bbdd76 --- /dev/null +++ b/vector/src/main/res/layout/item_space_top_summary.xml @@ -0,0 +1,46 @@ + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index c9cb4729f7..b296c4e5ea 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -508,6 +508,9 @@ Communities No groups + Invites + Spaces + Send logs Send crash logs Send key share requests history