diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt index 1302a34daa3..fd5fbf7bb0f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt @@ -28,5 +28,6 @@ data class SpaceChildInfo( val order: String?, val activeMemberCount: Int?, val autoJoin: Boolean, - val viaServers: List + val viaServers: List, + val parentRoomId: String? ) 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 15b6f7d852a..390f8c6718f 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 @@ -54,7 +54,8 @@ interface SpaceService { /** * Get's information of a space by querying the server */ - suspend fun querySpaceChildren(spaceId: String): Pair> + suspend fun querySpaceChildren(spaceId: String, suggestedOnly: Boolean? = null, autoJoinedOnly: Boolean? = null) + : Pair> /** * Get a live list of space summaries. This list is refreshed as soon as the data changes. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index 9da735cf6c6..af708c237c8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -86,7 +86,8 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa activeMemberCount = it.childSummaryEntity?.joinedMembersCount, order = it.order, autoJoin = it.autoJoin ?: false, - viaServers = it.viaServers.toList() + viaServers = it.viaServers.toList(), + parentRoomId = roomSummaryEntity.roomId ) } ) 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 ab104901d5f..02ebc266c0a 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 @@ -44,7 +44,6 @@ import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult -import java.lang.IllegalArgumentException import javax.inject.Inject internal class DefaultSpaceService @Inject constructor( @@ -90,8 +89,8 @@ internal class DefaultSpaceService @Inject constructor( return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId)) } - override suspend fun querySpaceChildren(spaceId: String): Pair> { - return resolveSpaceInfoTask.execute(ResolveSpaceInfoTask.Params.withId(spaceId)).let { response -> + override suspend fun querySpaceChildren(spaceId: String, suggestedOnly: Boolean?, autoJoinedOnly: Boolean?): Pair> { + return resolveSpaceInfoTask.execute(ResolveSpaceInfoTask.Params.withId(spaceId, suggestedOnly, autoJoinedOnly)).let { response -> val spaceDesc = response.rooms?.firstOrNull { it.roomId == spaceId } Pair( first = RoomSummary( @@ -111,7 +110,7 @@ internal class DefaultSpaceService @Inject constructor( ?.map { childSummary -> val childStateEv = response.events ?.firstOrNull { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD } - ?.content.toModel() + val childStateEvContent = childStateEv?.content.toModel() SpaceChildInfo( childRoomId = childSummary.roomId, isKnown = true, @@ -119,10 +118,11 @@ internal class DefaultSpaceService @Inject constructor( name = childSummary.name, topic = childSummary.topic, avatarUrl = childSummary.avatarUrl, - order = childStateEv?.order, - autoJoin = childStateEv?.autoJoin ?: false, - viaServers = childStateEv?.via ?: emptyList(), - activeMemberCount = childSummary.numJoinedMembers + order = childStateEvContent?.order, + autoJoin = childStateEvContent?.autoJoin ?: false, + viaServers = childStateEvContent?.via ?: emptyList(), + activeMemberCount = childSummary.numJoinedMembers, + parentRoomId = childStateEv?.roomId ) } ?: emptyList() ) 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 index 1eacdce8df5..4326b1934e1 100644 --- 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 @@ -23,12 +23,15 @@ import javax.inject.Inject internal interface ResolveSpaceInfoTask : Task { data class Params( val spaceId: String, - val maxRoomPerSpace: Int, + val maxRoomPerSpace: Int?, val limit: Int, - val batchToken: String? + val batchToken: String?, + val suggestedOnly: Boolean?, + val autoJoinOnly: Boolean? ) { companion object { - fun withId(spaceId: String) = Params(spaceId, 10, 20, null) + fun withId(spaceId: String, suggestedOnly: Boolean?, autoJoinOnly: Boolean?) = + Params(spaceId, 10, 20, null, suggestedOnly, autoJoinOnly) } } } @@ -37,7 +40,13 @@ 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 ?: "") + val body = SpaceSummaryParams( + maxRoomPerSpace = params.maxRoomPerSpace, + limit = params.limit, + batch = params.batchToken ?: "", + autoJoinedOnly = params.autoJoinOnly, + suggestedOnly = params.suggestedOnly + ) 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 index 249ebd2fd23..3a63cd0c015 100644 --- 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 @@ -29,6 +29,7 @@ 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. + * "auto_join_only": true, // If true, only return m.space.child events with auto_join:true, default: false, which returns all events. * "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: "". * } 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 index af5aec05540..c76987e66d2 100644 --- 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 @@ -22,12 +22,14 @@ 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, + @Json(name = "max_rooms_per_space") val maxRoomPerSpace: Int?, /** The maximum number of rooms/subspaces to return, server can override this, default: 100 */ - @Json(name = "limit") val limit: Int = 100, + @Json(name = "limit") val limit: Int?, /** A token to use if this is a subsequent HTTP hit, default: "".*/ @Json(name = "batch") val batch: String = "", /** whether we should only return children with the "suggested" flag set.*/ - @Json(name = "suggested_only") val suggestedOnly: Boolean = false + @Json(name = "suggested_only") val suggestedOnly: Boolean?, + /** whether we should only return children with the "suggested" flag set.*/ + @Json(name = "auto_join_only") val autoJoinedOnly: Boolean? ) diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index ea7ff9b3475..71fe1880be7 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -109,7 +109,7 @@ class AppStateHandler @Inject constructor( if (currentSpace != null) { val childInfo = withContext(Dispatchers.IO) { tryOrNull { - session.spaceService().querySpaceChildren(currentSpace.roomId) + session.spaceService().querySpaceChildren(currentSpace.roomId, suggestedOnly = true) } } childInfo?.second?.let { currentSpaceSuggestedDataSource.post(it) } ?: kotlin.run { diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index a1be337dc6d..9e2d249bbeb 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -123,6 +123,7 @@ import im.vector.app.features.spaces.SpaceListFragment import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment +import im.vector.app.features.spaces.explore.SpaceDirectoryFragment import im.vector.app.features.spaces.preview.SpacePreviewFragment import im.vector.app.features.terms.ReviewTermsFragment import im.vector.app.features.usercode.ShowUserCodeFragment @@ -666,4 +667,9 @@ interface FragmentModule { @IntoMap @FragmentKey(MatrixToRoomSpaceFragment::class) fun bindMatrixToRoomSpaceFragment(fragment: MatrixToRoomSpaceFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(SpaceDirectoryFragment::class) + fun bindSpaceDirectoryFragment(fragment: SpaceDirectoryFragment): Fragment } diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index f9ffc9c6121..b1c1e73081e 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -79,6 +79,7 @@ import im.vector.app.features.share.IncomingShareActivity import im.vector.app.features.signout.soft.SoftLogoutActivity import im.vector.app.features.spaces.ShareSpaceBottomSheet import im.vector.app.features.spaces.SpaceCreationActivity +import im.vector.app.features.spaces.SpaceExploreActivity import im.vector.app.features.terms.ReviewTermsActivity import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.usercode.UserCodeActivity @@ -154,6 +155,7 @@ interface ScreenComponent { fun inject(activity: ReAuthActivity) fun inject(activity: RoomDevToolActivity) fun inject(activity: SpaceCreationActivity) + fun inject(activity: SpaceExploreActivity) /* ========================================================================================== * BottomSheets diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index 7f27b13b593..b90e3cef39f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -57,12 +57,13 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor fun createSuggestion(spaceChildInfo: SpaceChildInfo, suggestedRoomJoiningStates: Map>, - onJoinClick: View.OnClickListener) : VectorEpoxyModel<*> { - return SuggestedRoomItem_() + onJoinClick: View.OnClickListener): VectorEpoxyModel<*> { + return SpaceChildInfoItem_() .id("sug_${spaceChildInfo.childRoomId}") .matrixItem(MatrixItem.RoomItem(spaceChildInfo.childRoomId, spaceChildInfo.name, spaceChildInfo.avatarUrl)) .avatarRenderer(avatarRenderer) .topic(spaceChildInfo.topic) + .buttonLabel(stringProvider.getString(R.string.join)) .loading(suggestedRoomJoiningStates[spaceChildInfo.childRoomId] is Loading) .memberCount(spaceChildInfo.activeMemberCount ?: 0) .buttonClickListener(onJoinClick) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt similarity index 87% rename from vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomItem.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt index bdd196f7503..cb9c8b1f2eb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt @@ -38,7 +38,7 @@ import me.gujun.android.span.span import org.matrix.android.sdk.api.util.MatrixItem @EpoxyModelClass(layout = R.layout.item_suggested_room) -abstract class SuggestedRoomItem : VectorEpoxyModel() { +abstract class SpaceChildInfoItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem @@ -48,6 +48,9 @@ abstract class SuggestedRoomItem : VectorEpoxyModel() @EpoxyAttribute var memberCount: Int = 0 @EpoxyAttribute var loading: Boolean = false + @EpoxyAttribute var space: Boolean = false + + @EpoxyAttribute var buttonLabel: String? = null @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemLongClickListener: View.OnLongClickListener? = null @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: View.OnClickListener? = null @@ -61,13 +64,17 @@ abstract class SuggestedRoomItem : VectorEpoxyModel() itemLongClickListener?.onLongClick(it) ?: false } holder.titleView.text = matrixItem.getBestName() - avatarRenderer.render(matrixItem, holder.avatarImageView) + if (space) { + avatarRenderer.renderSpace(matrixItem, holder.avatarImageView) + } else { + avatarRenderer.render(matrixItem, holder.avatarImageView) + } holder.descriptionText.text = span { span { apply { val tintColor = ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_secondary) - ContextCompat.getDrawable(holder.view.context, R.drawable.ic_room_profile_member_list) + ContextCompat.getDrawable(holder.view.context, R.drawable.ic_member_small) ?.apply { ThemeUtils.tintDrawableWithColor(this, tintColor) }?.let { @@ -83,6 +90,8 @@ abstract class SuggestedRoomItem : VectorEpoxyModel() } } + holder.joinButton.text = buttonLabel + if (loading) { holder.joinButtonLoading.isVisible = true holder.joinButton.isInvisible = true @@ -93,8 +102,8 @@ abstract class SuggestedRoomItem : VectorEpoxyModel() holder.joinButton.setOnClickListener { // local echo - holder.joinButtonLoading.isVisible = true - holder.joinButton.isInvisible = true + holder.joinButton.isEnabled = false + holder.view.postDelayed({ holder.joinButton.isEnabled = true }, 400) buttonClickListener?.onClick(it) } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 548f0b986db..c2986dfbc27 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -71,6 +71,7 @@ import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.share.SharedData +import im.vector.app.features.spaces.SpaceExploreActivity import im.vector.app.features.spaces.SpacePreviewActivity import im.vector.app.features.terms.ReviewTermsActivity import im.vector.app.features.widgets.WidgetActivity @@ -250,8 +251,17 @@ class DefaultNavigator @Inject constructor( } override fun openRoomDirectory(context: Context, initialFilter: String) { - val intent = RoomDirectoryActivity.getIntent(context, initialFilter) - context.startActivity(intent) + val selectedSpace = selectedSpaceDataSource.currentValue?.orNull()?.let { + sessionHolder.getSafeActiveSession()?.getRoomSummary(it.roomId) + } + if (selectedSpace == null) { + val intent = RoomDirectoryActivity.getIntent(context, initialFilter) + context.startActivity(intent) + } else { + SpaceExploreActivity.newIntent(context, selectedSpace.roomId).let { + context.startActivity(it) + } + } } override fun openCreateRoom(context: Context, initialName: String) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt index 298f51922b6..e07896de502 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt @@ -20,47 +20,60 @@ import android.content.Context import android.content.Intent import android.os.Bundle import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.viewModel import im.vector.app.R +import im.vector.app.core.di.ScreenComponent 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.explore.SpaceDirectoryArgs import im.vector.app.features.spaces.explore.SpaceDirectoryFragment -import im.vector.app.features.spaces.preview.SpacePreviewArgs -import im.vector.app.features.spaces.preview.SpacePreviewFragment +import im.vector.app.features.spaces.explore.SpaceDirectoryState +import im.vector.app.features.spaces.explore.SpaceDirectoryViewEvents +import im.vector.app.features.spaces.explore.SpaceDirectoryViewModel +import javax.inject.Inject -class SpaceExploreActivity : VectorBaseActivity() { +class SpaceExploreActivity : VectorBaseActivity(), SpaceDirectoryViewModel.Factory { + + @Inject lateinit var spaceDirectoryViewModelFactory: SpaceDirectoryViewModel.Factory + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } override fun getBinding(): ActivitySimpleBinding = ActivitySimpleBinding.inflate(layoutInflater) - // lateinit var sharedActionViewModel: SpacePreviewSharedActionViewModel + + override fun getTitleRes(): Int = R.string.space_explore_activity_title + + val sharedViewModel: SpaceDirectoryViewModel by viewModel() 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 = SpaceDirectoryFragment::class.java.simpleName - val args = intent?.getParcelableExtra(MvRx.KEY_ARG) + val args = intent?.getParcelableExtra(MvRx.KEY_ARG) if (supportFragmentManager.findFragmentByTag(simpleName) == null) { supportFragmentManager.commitTransaction { replace(R.id.simpleFragmentContainer, - SpacePreviewFragment::class.java, + SpaceDirectoryFragment::class.java, Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) }, simpleName ) } } } + + sharedViewModel.observeViewEvents { + when (it) { + SpaceDirectoryViewEvents.Dismiss -> { + finish() + } + is SpaceDirectoryViewEvents.NavigateToRoom -> { + navigator.openRoom(this, it.roomId) + } + } + } } companion object { @@ -70,4 +83,7 @@ class SpaceExploreActivity : VectorBaseActivity() { } } } + + override fun create(initialState: SpaceDirectoryState): SpaceDirectoryViewModel = + spaceDirectoryViewModelFactory.create(initialState) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt index 2c46607987e..44d98766d5c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt @@ -16,38 +16,77 @@ package im.vector.app.features.spaces.explore +import android.view.View import com.airbnb.epoxy.TypedEpoxyController -import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete -import com.airbnb.mvrx.Success -import im.vector.app.core.epoxy.errorWithRetryItem +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.features.home.AvatarRenderer +import im.vector.app.features.home.room.list.spaceChildInfoItem +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.util.MatrixItem +import javax.inject.Inject -class SpaceDirectoryController : TypedEpoxyController() { +class SpaceDirectoryController @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider +) : TypedEpoxyController() { + + interface InteractionListener { + fun onButtonClick(spaceChildInfo: SpaceChildInfo) + fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo) + } + + var listener: InteractionListener? = null override fun buildModels(data: SpaceDirectoryState?) { - when (data?.summary) { - is Success -> { -// val directories = roomDirectoryListCreator.computeDirectories(asyncThirdPartyProtocol()) -// -// directories.forEach { -// buildDirectory(it) -// } + val results = data?.spaceSummaryApiResult + + if (results is Incomplete) { + loadingItem { + id("loading") } - is Incomplete -> { - loadingItem { - id("loading") + } else { + val flattenChildInfo = results?.invoke() + ?.filter { + it.parentRoomId == (data.hierarchyStack.lastOrNull() ?: data.spaceId) + } + ?: emptyList() + + if (flattenChildInfo.isEmpty()) { + genericFooterItem { + id("empty_footer") + stringProvider.getString(R.string.no_result_placeholder) } - } - is Fail -> { - errorWithRetryItem { - id("error") -// text(errorFormatter.toHumanReadable(asyncThirdPartyProtocol.error)) -// listener { callback?.retry() } + } else { + flattenChildInfo.forEach { info -> + val isSpace = info.roomType == RoomType.SPACE + val isJoined = data?.joinedRoomsIds?.contains(info.childRoomId) == true + val isLoading = data?.changeMembershipStates?.get(info.childRoomId)?.isInProgress() ?: false + spaceChildInfoItem { + id(info.childRoomId) + matrixItem(MatrixItem.RoomItem(info.childRoomId, info.name, info.avatarUrl)) + avatarRenderer(avatarRenderer) + topic(info.topic) + memberCount(info.activeMemberCount ?: 0) + space(isSpace) + loading(isLoading) + buttonLabel( + if (isJoined) stringProvider.getString(R.string.action_open) + else stringProvider.getString(R.string.join) + ) + apply { + if (isSpace) { + itemClickListener(View.OnClickListener { listener?.onSpaceChildClick(info) }) + } + } + buttonClickListener(View.OnClickListener { listener?.onButtonClick(info) }) + } } } - else -> { - } } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt index d776d9ff51e..567012675e2 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt @@ -16,20 +16,74 @@ package im.vector.app.features.spaces.explore +import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import javax.inject.Inject @Parcelize data class SpaceDirectoryArgs( val spaceId: String ) : Parcelable -class SpaceDirectoryFragment : VectorBaseFragment() { +class SpaceDirectoryFragment @Inject constructor( + private val epoxyController: SpaceDirectoryController +) : VectorBaseFragment(), + SpaceDirectoryController.InteractionListener, + OnBackPressed { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentRoomDirectoryPickerBinding.inflate(layoutInflater, container, false) + + private val viewModel by activityViewModel(SpaceDirectoryViewModel::class) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + vectorBaseActivity.setSupportActionBar(views.toolbar) + + vectorBaseActivity.supportActionBar?.let { + it.setDisplayShowHomeEnabled(true) + it.setDisplayHomeAsUpEnabled(true) + } + epoxyController.listener = this + views.roomDirectoryPickerList.configureWith(epoxyController) + } + + override fun onDestroyView() { + epoxyController.listener = null + views.roomDirectoryPickerList.cleanup() + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { + epoxyController.setData(it) + } + + override fun onButtonClick(spaceChildInfo: SpaceChildInfo) { + viewModel.handle(SpaceDirectoryViewAction.JoinOrOpen(spaceChildInfo)) + } + + override fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo) { + if (spaceChildInfo.roomType == RoomType.SPACE) { + viewModel.handle(SpaceDirectoryViewAction.ExploreSubSpace(spaceChildInfo)) + } + } + + override fun onBackPressed(toolbarButton: Boolean): Boolean { + viewModel.handle(SpaceDirectoryViewAction.HandleBack) + return true + } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt index 31f7162c2a6..107ce9ee197 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt @@ -19,9 +19,12 @@ package im.vector.app.features.spaces.explore import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted @@ -30,38 +33,54 @@ import dagger.assisted.AssistedInject import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction -import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap +import timber.log.Timber data class SpaceDirectoryState( // The current filter val spaceId: String, val currentFilter: String = "", - val summary: Async = Uninitialized, + val spaceSummary: Async = Uninitialized, + val spaceSummaryApiResult: Async> = Uninitialized, + val childList: List = emptyList(), + val hierarchyStack: List = emptyList(), // True if more result are available server side val hasMore: Boolean = false, // Set of joined roomId / spaces, - val joinedRoomsIds: Set = emptySet() + val joinedRoomsIds: Set = emptySet(), + // keys are room alias or roomId + val changeMembershipStates: Map = emptyMap() ) : MvRxState { - constructor(args: SpaceDirectoryArgs) : this(spaceId = args.spaceId) + constructor(args: SpaceDirectoryArgs) : this( + spaceId = args.spaceId + ) } -sealed class SpaceDirectoryViewAction : VectorViewModelAction +sealed class SpaceDirectoryViewAction : VectorViewModelAction { + data class ExploreSubSpace(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction() + data class JoinOrOpen(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction() + object HandleBack : SpaceDirectoryViewAction() +} -sealed class SpaceDirectoryViewEvents : VectorViewEvents +sealed class SpaceDirectoryViewEvents : VectorViewEvents { + object Dismiss : SpaceDirectoryViewEvents() + data class NavigateToRoom(val roomId: String) : SpaceDirectoryViewEvents() +} class SpaceDirectoryViewModel @AssistedInject constructor( @Assisted initialState: SpaceDirectoryState, private val session: Session -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { @AssistedFactory interface Factory { @@ -79,24 +98,113 @@ class SpaceDirectoryViewModel @AssistedInject constructor( } init { - val queryParams = roomSummaryQueryParams { - roomId = QueryStringValue.Equals(initialState.spaceId) + + val spaceSum = session.getRoomSummary(initialState.spaceId) + setState { + copy( + childList = spaceSum?.children ?: emptyList(), + spaceSummaryApiResult = Loading() + ) } viewModelScope.launch(Dispatchers.IO) { - session - .rx() - .liveSpaceSummaries(queryParams) - .observeOn(Schedulers.computation()) - .map { sum -> Optional.from(sum.firstOrNull()) } - .unwrap() - .execute { async -> - copy(summary = async) + try { + val query = session.spaceService().querySpaceChildren(initialState.spaceId) + setState { + copy( + spaceSummaryApiResult = Success(query.second) + ) + } + } catch (failure: Throwable) { + setState { + copy( + spaceSummaryApiResult = Fail(failure) + ) + } + } + } + observeJoinedRooms() + observeMembershipChanges() + } + + private fun observeJoinedRooms() { + val queryParams = roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + } + session + .rx() + .liveRoomSummaries(queryParams) + .subscribe { list -> + val joinedRoomIds = list + ?.map { it.roomId } + ?.toSet() + ?: emptySet() + + setState { + copy(joinedRoomsIds = joinedRoomIds) } + } + .disposeOnClear() + } + + private fun observeMembershipChanges() { + session.rx() + .liveRoomChangeMembershipState() + .subscribe { + setState { copy(changeMembershipStates = it) } + } + .disposeOnClear() + } + + override fun handle(action: SpaceDirectoryViewAction) { + when (action) { + is SpaceDirectoryViewAction.ExploreSubSpace -> { + setState { + copy(hierarchyStack = hierarchyStack + listOf(action.spaceChildInfo.childRoomId)) + } + } + SpaceDirectoryViewAction.HandleBack -> { + withState { + if (it.hierarchyStack.isEmpty()) { + _viewEvents.post(SpaceDirectoryViewEvents.Dismiss) + } else { + setState { + copy( + hierarchyStack = hierarchyStack.dropLast(1) + ) + } + } + } + } + is SpaceDirectoryViewAction.JoinOrOpen -> { + handleJoinOrOpen(action.spaceChildInfo) + } } } - override fun handle(action: VectorViewModelAction) { - TODO("Not yet implemented") + private fun handleJoinOrOpen(spaceChildInfo: SpaceChildInfo) = withState { state -> + val isSpace = spaceChildInfo.roomType == RoomType.SPACE + if (state.joinedRoomsIds.contains(spaceChildInfo.childRoomId)) { + if (isSpace) { + handle(SpaceDirectoryViewAction.ExploreSubSpace(spaceChildInfo)) + } else { + _viewEvents.post(SpaceDirectoryViewEvents.NavigateToRoom(spaceChildInfo.childRoomId)) + } + } else { + // join + viewModelScope.launch { + try { + if (isSpace) { + session.spaceService().joinSpace(spaceChildInfo.childRoomId, null, spaceChildInfo.viaServers) + } else { + awaitCallback { + session.joinRoom(spaceChildInfo.childRoomId, null, spaceChildInfo.viaServers, it) + } + } + } catch (failure: Throwable) { + Timber.e(failure, "## Space: Failed to join room or subsapce") + } + } + } } } diff --git a/vector/src/main/res/drawable/ic_member_small.xml b/vector/src/main/res/drawable/ic_member_small.xml new file mode 100644 index 00000000000..9298c5f80c8 --- /dev/null +++ b/vector/src/main/res/drawable/ic_member_small.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/fragment_room_directory_picker.xml b/vector/src/main/res/layout/fragment_room_directory_picker.xml index 9a4a306a009..f0b7a22b8cf 100644 --- a/vector/src/main/res/layout/fragment_room_directory_picker.xml +++ b/vector/src/main/res/layout/fragment_room_directory_picker.xml @@ -32,5 +32,4 @@ tools:listitem="@layout/item_room_directory" /> - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_suggested_room.xml b/vector/src/main/res/layout/item_suggested_room.xml index 71b41ff17dd..7a3b73487c2 100644 --- a/vector/src/main/res/layout/item_suggested_room.xml +++ b/vector/src/main/res/layout/item_suggested_room.xml @@ -5,6 +5,7 @@ android:id="@+id/itemRoomLayout" android:layout_width="match_parent" android:layout_height="wrap_content" + android:foreground="?attr/selectableItemBackground" android:background="?riotx_background"> You’re not in any rooms yet. Below are some suggested rooms, but you can see more with the green button bottom right. + Explore rooms