Basic support to browse space directory

This commit is contained in:
Valere 2021-03-19 10:24:26 +01:00
parent 802853d205
commit 06a84d985a
20 changed files with 361 additions and 90 deletions

View file

@ -28,5 +28,6 @@ data class SpaceChildInfo(
val order: String?,
val activeMemberCount: Int?,
val autoJoin: Boolean,
val viaServers: List<String>
val viaServers: List<String>,
val parentRoomId: String?
)

View file

@ -54,7 +54,8 @@ interface SpaceService {
/**
* Get's information of a space by querying the server
*/
suspend fun querySpaceChildren(spaceId: String): Pair<RoomSummary, List<SpaceChildInfo>>
suspend fun querySpaceChildren(spaceId: String, suggestedOnly: Boolean? = null, autoJoinedOnly: Boolean? = null)
: Pair<RoomSummary, List<SpaceChildInfo>>
/**
* Get a live list of space summaries. This list is refreshed as soon as the data changes.

View file

@ -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
)
}
)

View file

@ -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<RoomSummary, List<SpaceChildInfo>> {
return resolveSpaceInfoTask.execute(ResolveSpaceInfoTask.Params.withId(spaceId)).let { response ->
override suspend fun querySpaceChildren(spaceId: String, suggestedOnly: Boolean?, autoJoinedOnly: Boolean?): Pair<RoomSummary, List<SpaceChildInfo>> {
return resolveSpaceInfoTask.execute(ResolveSpaceInfoTask.Params.withId(spaceId, suggestedOnly, autoJoinedOnly)).let { response ->
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<SpaceChildContent>()
val childStateEvContent = childStateEv?.content.toModel<SpaceChildContent>()
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()
)

View file

@ -23,12 +23,15 @@ import javax.inject.Inject
internal interface ResolveSpaceInfoTask : Task<ResolveSpaceInfoTask.Params, SpacesResponse> {
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<SpacesResponse>(null) {
apiCall = spaceApi.getSpaces(params.spaceId, body)
}

View file

@ -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: "".
* }

View file

@ -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?
)

View file

@ -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
}

View file

@ -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

View file

@ -57,12 +57,13 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
fun createSuggestion(spaceChildInfo: SpaceChildInfo,
suggestedRoomJoiningStates: Map<String, Async<Unit>>,
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)

View file

@ -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<SuggestedRoomItem.Holder>() {
abstract class SpaceChildInfoItem : VectorEpoxyModel<SpaceChildInfoItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@ -48,6 +48,9 @@ abstract class SuggestedRoomItem : VectorEpoxyModel<SuggestedRoomItem.Holder>()
@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<SuggestedRoomItem.Holder>()
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<SuggestedRoomItem.Holder>()
}
}
holder.joinButton.text = buttonLabel
if (loading) {
holder.joinButtonLoading.isVisible = true
holder.joinButton.isInvisible = true
@ -93,8 +102,8 @@ abstract class SuggestedRoomItem : VectorEpoxyModel<SuggestedRoomItem.Holder>()
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)
}
}

View file

@ -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) {

View file

@ -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<ActivitySimpleBinding>() {
class SpaceExploreActivity : VectorBaseActivity<ActivitySimpleBinding>(), 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<SpacePreviewArgs>(MvRx.KEY_ARG)
val args = intent?.getParcelableExtra<SpaceDirectoryArgs>(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<ActivitySimpleBinding>() {
}
}
}
override fun create(initialState: SpaceDirectoryState): SpaceDirectoryViewModel =
spaceDirectoryViewModelFactory.create(initialState)
}

View file

@ -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<SpaceDirectoryState>() {
class SpaceDirectoryController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider
) : TypedEpoxyController<SpaceDirectoryState>() {
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 -> {
}
}
}
}

View file

@ -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<FragmentRoomDirectoryPickerBinding>() {
class SpaceDirectoryFragment @Inject constructor(
private val epoxyController: SpaceDirectoryController
) : VectorBaseFragment<FragmentRoomDirectoryPickerBinding>(),
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
}
}

View file

@ -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<RoomSummary> = Uninitialized,
val spaceSummary: Async<RoomSummary> = Uninitialized,
val spaceSummaryApiResult: Async<List<SpaceChildInfo>> = Uninitialized,
val childList: List<SpaceChildInfo> = emptyList(),
val hierarchyStack: List<String> = emptyList(),
// True if more result are available server side
val hasMore: Boolean = false,
// Set of joined roomId / spaces,
val joinedRoomsIds: Set<String> = emptySet()
val joinedRoomsIds: Set<String> = emptySet(),
// keys are room alias or roomId
val changeMembershipStates: Map<String, ChangeMembershipState> = 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<SpaceDirectoryState, VectorViewModelAction, SpaceDirectoryViewEvents>(initialState) {
) : VectorViewModel<SpaceDirectoryState, SpaceDirectoryViewAction, SpaceDirectoryViewEvents>(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<Unit> {
session.joinRoom(spaceChildInfo.childRoomId, null, spaceChildInfo.viaServers, it)
}
}
} catch (failure: Throwable) {
Timber.e(failure, "## Space: Failed to join room or subsapce")
}
}
}
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M16,8C16,12.4183 12.4183,16 8,16C3.5817,16 0,12.4183 0,8C0,3.5817 3.5817,0 8,0C12.4183,0 16,3.5817 16,8ZM7.3333,6.1667C7.3333,7.3633 6.4379,8.3333 5.3333,8.3333C4.2288,8.3333 3.3333,7.3633 3.3333,6.1667C3.3333,4.97 4.2288,4 5.3333,4C6.4379,4 7.3333,4.97 7.3333,6.1667ZM13.2564,7.2315C13.2564,8.2798 12.472,9.1296 11.5043,9.1296C10.5366,9.1296 9.7522,8.2798 9.7522,7.2315C9.7522,6.1832 10.5366,5.3333 11.5043,5.3333C12.472,5.3333 13.2564,6.1832 13.2564,7.2315ZM8.6666,12.5969C8.6666,11.0353 7.5698,9.7298 6.1045,9.4089C5.855,9.3593 5.5971,9.3333 5.3333,9.3333C3.9862,9.3333 2.7946,10.0107 2.0698,11.0489C3.1767,13.1974 5.4167,14.6667 8,14.6667C8.225,14.6667 8.4474,14.6555 8.6666,14.6338L8.6666,12.5969ZM13.9475,11.015C13.1422,12.6005 11.7216,13.8208 10,14.3615L10,12.5969C10,11.8856 9.8385,11.2121 9.5501,10.611C10.1084,10.2288 10.7807,10.0057 11.5043,10.0057C12.4545,10.0057 13.3163,10.3904 13.9475,11.015Z"
android:fillColor="#737D8C"
android:fillType="evenOdd"/>
</vector>

View file

@ -32,5 +32,4 @@
tools:listitem="@layout/item_room_directory" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -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">
<FrameLayout

View file

@ -3294,4 +3294,5 @@
<string name="suggested_rooms_pills_on_empty_text">Youre not in any rooms yet. Below are some suggested rooms, but you can see more with the green button bottom right.</string>
<string name="space_explore_activity_title">Explore rooms</string>
</resources>