Merge pull request #3312 from vector-im/feature/bca/spaces_polish

Spaces various fixes and polish
This commit is contained in:
Benoit Marty 2021-05-12 10:09:36 +02:00 committed by GitHub
commit 56d0699bc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 736 additions and 185 deletions

View File

@ -33,7 +33,7 @@ interface Space {
fun spaceSummary(): RoomSummary?
suspend fun addChildren(roomId: String,
viaServers: List<String>,
viaServers: List<String>?,
order: String?,
autoJoin: Boolean = false,
suggested: Boolean? = false)

View File

@ -18,19 +18,13 @@ package org.matrix.android.sdk.internal.session.permalinks
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomGetter
import java.net.URLEncoder
import javax.inject.Inject
import javax.inject.Provider
internal class PermalinkFactory @Inject constructor(
@UserId
private val userId: String,
// Use a provider to fix circular Dagger dependency
private val roomGetterProvider: Provider<RoomGetter>
private val viaParameterFinder: ViaParameterFinder
) {
fun createPermalink(event: Event): String? {
@ -50,12 +44,12 @@ internal class PermalinkFactory @Inject constructor(
return if (roomId.isEmpty()) {
null
} else {
MATRIX_TO_URL_BASE + escape(roomId) + computeViaParams(userId, roomId)
MATRIX_TO_URL_BASE + escape(roomId) + viaParameterFinder.computeViaParams(userId, roomId)
}
}
fun createPermalink(roomId: String, eventId: String): String {
return MATRIX_TO_URL_BASE + escape(roomId) + "/" + escape(eventId) + computeViaParams(userId, roomId)
return MATRIX_TO_URL_BASE + escape(roomId) + "/" + escape(eventId) + viaParameterFinder.computeViaParams(userId, roomId)
}
fun getLinkedId(url: String): String? {
@ -66,25 +60,6 @@ internal class PermalinkFactory @Inject constructor(
} else null
}
/**
* Compute the via parameters.
* Take up to 3 homeserver domains, taking the most representative one regarding room members and including the
* current user one.
*/
private fun computeViaParams(userId: String, roomId: String): String {
val userHomeserver = userId.substringAfter(":")
return getUserIdsOfJoinedMembers(roomId)
.map { it.substringAfter(":") }
.groupBy { it }
.mapValues { it.value.size }
.toMutableMap()
// Ensure the user homeserver will be included
.apply { this[userHomeserver] = Int.MAX_VALUE }
.let { map -> map.keys.sortedByDescending { map[it] } }
.take(3)
.joinToString(prefix = "?via=", separator = "&via=") { URLEncoder.encode(it, "utf-8") }
}
/**
* Escape '/' in id, because it is used as a separator
*
@ -104,15 +79,4 @@ internal class PermalinkFactory @Inject constructor(
private fun unescape(id: String): String {
return id.replace("%2F", "/")
}
/**
* Get a set of userIds of joined members of a room
*/
private fun getUserIdsOfJoinedMembers(roomId: String): Set<String> {
return roomGetterProvider.get().getRoom(roomId)
?.getRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.JOIN) })
?.map { it.userId }
.orEmpty()
.toSet()
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.permalinks
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomGetter
import java.net.URLEncoder
import javax.inject.Inject
import javax.inject.Provider
internal class ViaParameterFinder @Inject constructor(
@UserId private val userId: String,
private val roomGetterProvider: Provider<RoomGetter>
) {
fun computeViaParams(roomId: String, max: Int): List<String> {
return computeViaParams(userId, roomId, max)
}
/**
* Compute the via parameters.
* Take up to 3 homeserver domains, taking the most representative one regarding room members and including the
* current user one.
*/
fun computeViaParams(userId: String, roomId: String): String {
return computeViaParams(userId, roomId, 3)
.joinToString(prefix = "?via=", separator = "&via=") { URLEncoder.encode(it, "utf-8") }
}
fun computeViaParams(userId: String, roomId: String, max: Int): List<String> {
val userHomeserver = userId.substringAfter(":")
return getUserIdsOfJoinedMembers(roomId)
.map { it.substringAfter(":") }
.groupBy { it }
.mapValues { it.value.size }
.toMutableMap()
// Ensure the user homeserver will be included
.apply { this[userHomeserver] = Int.MAX_VALUE }
.let { map -> map.keys.sortedByDescending { map[it] } }
.take(max)
}
/**
* Get a set of userIds of joined members of a room
*/
private fun getUserIdsOfJoinedMembers(roomId: String): Set<String> {
return roomGetterProvider.get().getRoom(roomId)
?.getRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.JOIN) })
?.map { it.userId }
.orEmpty()
.toSet()
}
}

View File

@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.search.SearchResult
import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.search.SearchTask
@ -66,6 +67,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
private val roomMembersService: MembershipService,
private val roomPushRuleService: RoomPushRuleService,
private val sendStateTask: SendStateTask,
private val viaParameterFinder: ViaParameterFinder,
private val searchTask: SearchTask) :
Room,
TimelineService by timelineService,
@ -154,6 +156,6 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
override fun asSpace(): Space? {
if (roomSummary()?.roomType != RoomType.SPACE) return null
return DefaultSpace(this, roomSummaryDataSource)
return DefaultSpace(this, roomSummaryDataSource, viaParameterFinder)
}
}

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder
import org.matrix.android.sdk.internal.session.room.alias.DefaultAliasService
import org.matrix.android.sdk.internal.session.room.call.DefaultRoomCallService
import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService
@ -60,6 +61,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
private val membershipServiceFactory: DefaultMembershipService.Factory,
private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory,
private val sendStateTask: SendStateTask,
private val viaParameterFinder: ViaParameterFinder,
private val searchTask: SearchTask) :
RoomFactory {
@ -83,7 +85,8 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
roomMembersService = membershipServiceFactory.create(roomId),
roomPushRuleService = roomPushRuleServiceFactory.create(roomId),
sendStateTask = sendStateTask,
searchTask = searchTask
searchTask = searchTask,
viaParameterFinder = viaParameterFinder
)
}
}

View File

@ -364,6 +364,8 @@ internal class RoomSummaryUpdater @Inject constructor(
realm.where(RoomSummaryEntity::class.java)
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, listOf(Membership.JOIN))
.notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE)
// also we do not count DM in here, because home space will already show them
.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, space.roomId)
.findAll().forEach {
highlightCount += it.highlightCount

View File

@ -24,11 +24,13 @@ import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
internal class DefaultSpace(
private val room: Room,
private val spaceSummaryDataSource: RoomSummaryDataSource
private val spaceSummaryDataSource: RoomSummaryDataSource,
private val viaParameterFinder: ViaParameterFinder
) : Space {
override fun asRoom(): Room {
@ -46,15 +48,17 @@ internal class DefaultSpace(
}
override suspend fun addChildren(roomId: String,
viaServers: List<String>,
viaServers: List<String>?,
order: String?,
autoJoin: Boolean,
suggested: Boolean?) {
// Find best via
room.sendStateEvent(
eventType = EventType.STATE_SPACE_CHILD,
stateKey = roomId,
body = SpaceChildContent(
via = viaServers,
via = viaServers ?: viaParameterFinder.computeViaParams(roomId, 3),
autoJoin = autoJoin,
order = order,
suggested = suggested

View File

@ -0,0 +1,88 @@
/*
* Copyright 2019 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.core.ui.list
import android.content.res.ColorStateList
import android.view.Gravity
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
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.themes.ThemeUtils
/**
* A generic list item with a rounded corner background and an optional icon
*/
@EpoxyModelClass(layout = R.layout.item_generic_pill_footer)
abstract class GenericPillItem : VectorEpoxyModel<GenericPillItem.Holder>() {
@EpoxyAttribute
var text: CharSequence? = null
@EpoxyAttribute
var style: ItemStyle = ItemStyle.NORMAL_TEXT
@EpoxyAttribute
var itemClickAction: GenericItem.Action? = null
@EpoxyAttribute
var centered: Boolean = false
@EpoxyAttribute
@DrawableRes
var imageRes: Int? = null
@EpoxyAttribute
var tintIcon: Boolean = true
override fun bind(holder: Holder) {
super.bind(holder)
holder.textView.setTextOrHide(text)
holder.textView.typeface = style.toTypeFace()
holder.textView.textSize = style.toTextSize()
holder.textView.gravity = if (centered) Gravity.CENTER_HORIZONTAL else Gravity.START
if (imageRes != null) {
holder.imageView.setImageResource(imageRes!!)
holder.imageView.isVisible = true
} else {
holder.imageView.isVisible = false
}
if (tintIcon) {
val iconTintColor = ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_secondary)
ImageViewCompat.setImageTintList(holder.imageView, ColorStateList.valueOf(iconTintColor))
} else {
ImageViewCompat.setImageTintList(holder.imageView, null)
}
holder.view.setOnClickListener {
itemClickAction?.perform?.run()
}
}
class Holder : VectorEpoxyHolder() {
val imageView by bind<ImageView>(R.id.itemGenericPillImage)
val textView by bind<TextView>(R.id.itemGenericPillText)
}
}

View File

@ -18,6 +18,7 @@
package im.vector.app.features.grouplist
import android.content.res.ColorStateList
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
@ -35,8 +36,9 @@ import im.vector.app.features.themes.ThemeUtils
abstract class HomeSpaceSummaryItem : VectorEpoxyModel<HomeSpaceSummaryItem.Holder>() {
@EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: (() -> Unit)? = null
@EpoxyAttribute var countState : UnreadCounterBadgeView.State = UnreadCounterBadgeView.State(0, false)
@EpoxyAttribute var showSeparator: Boolean = false
override fun getViewType(): Int {
// mm.. it's reusing the same layout for basic space item
@ -56,6 +58,7 @@ abstract class HomeSpaceSummaryItem : VectorEpoxyModel<HomeSpaceSummaryItem.Hold
holder.leaveView.isVisible = false
holder.counterBadgeView.render(countState)
holder.bottomSeparator.isVisible = showSeparator
}
class Holder : VectorEpoxyHolder() {
@ -64,5 +67,6 @@ abstract class HomeSpaceSummaryItem : VectorEpoxyModel<HomeSpaceSummaryItem.Hold
val rootView by bind<CheckableConstraintLayout>(R.id.itemGroupLayout)
val leaveView by bind<ImageView>(R.id.groupTmpLeave)
val counterBadgeView by bind<UnreadCounterBadgeView>(R.id.groupCounterBadge)
val bottomSeparator by bind<View>(R.id.groupBottomSeparator)
}
}

View File

@ -202,7 +202,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
setState {
copy(
notificationCountCatchup = dmRooms.totalCount + otherRooms.totalCount + roomsInvite + dmInvites,
notificationHighlightCatchup = dmRooms.isHighlight || otherRooms.isHighlight,
notificationHighlightCatchup = dmRooms.isHighlight || otherRooms.isHighlight || (dmInvites + roomsInvite) > 0,
notificationCountPeople = dmRooms.totalCount + dmInvites,
notificationHighlightPeople = dmRooms.isHighlight || dmInvites > 0,
notificationCountRooms = otherRooms.totalCount + roomsInvite,

View File

@ -29,6 +29,7 @@ import im.vector.app.RoomGroupingMethod
import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.settings.VectorPreferences
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
@ -52,6 +53,7 @@ data class CountInfo(
class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initialState: UnreadMessagesState,
session: Session,
private val vectorPreferences: VectorPreferences,
appStateHandler: AppStateHandler)
: VectorViewModel<UnreadMessagesState, EmptyAction, EmptyViewEvents>(initialState) {
@ -126,12 +128,24 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia
}
is RoomGroupingMethod.BySpace -> {
val selectedSpace = appStateHandler.safeActiveSpaceId()
val counts = session.getNotificationCountForRooms(
val inviteCount = session.getRoomSummaries(roomSummaryQueryParams {
this.memberships = listOf(Membership.INVITE)
}).size
val totalCount = session.getNotificationCountForRooms(
roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf {
vectorPreferences.labsSpacesOnlyOrphansInHome()
} ?: ActiveSpaceFilter.None
}
)
val counts = RoomAggregateNotificationCount(
totalCount.notificationCount + inviteCount,
totalCount.highlightCount + inviteCount
)
val rootCounts = session.spaceService().getRootSpaceSummaries()
.filter {
// filter out current selection

View File

@ -832,7 +832,7 @@ class RoomDetailViewModel @AssistedInject constructor(
session.spaceService().getSpace(spaceId)
?.addChildren(
state.roomId,
listOf(session.sessionParams.homeServerHost ?: ""),
null,
null,
true
)
@ -849,7 +849,7 @@ class RoomDetailViewModel @AssistedInject constructor(
session.spaceService().getSpace(slashCommandResult.spaceId)
?.addChildren(
room.roomId,
listOf(session.sessionParams.homeServerHost ?: ""),
null,
null,
false
)

View File

@ -69,12 +69,12 @@ class RoomListViewModel @Inject constructor(
* Filter the rooms if they are part of the current space (children and grand children).
* If current space is null, will return orphan rooms only
*/
NORMAL,
ORPHANS_IF_SPACE_NULL,
/**
* Special case when we don't want to discriminate rooms when current space is null.
* In this case return all.
*/
NOT_IF_ALL,
ALL_IF_SPACE_NULL,
/** Do not filter based on space*/
NONE
}
@ -131,7 +131,8 @@ class RoomListViewModel @Inject constructor(
},
{
updatableQuery = it
}
},
vectorPreferences.labsSpacesOnlyOrphansInHome()
).buildSections(initialState.displayMode)
} else {
GroupRoomListSectionBuilder(

View File

@ -41,6 +41,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.rx.asObservable
class SpaceRoomListSectionBuilder(
@ -50,7 +51,8 @@ class SpaceRoomListSectionBuilder(
val viewModelScope: CoroutineScope,
private val suggestedRoomJoiningState: LiveData<Map<String, Async<Unit>>>,
val onDisposable: (Disposable) -> Unit,
val onUdpatable: (UpdatableLivePageResult) -> Unit
val onUdpatable: (UpdatableLivePageResult) -> Unit,
val onlyOrphansInHome: Boolean = false
) : RoomListSectionBuilder {
val pagedListConfig = PagedList.Config.Builder()
@ -87,22 +89,31 @@ class SpaceRoomListSectionBuilder(
}
RoomListDisplayMode.NOTIFICATIONS -> {
addSection(
sections,
activeSpaceAwareQueries,
R.string.invitations_header,
true,
RoomListViewModel.SpaceFilterStrategy.NORMAL
sections = sections,
activeSpaceUpdaters = activeSpaceAwareQueries,
nameRes = R.string.invitations_header,
notifyOfLocalEcho = true,
spaceFilterStrategy = if (onlyOrphansInHome) {
RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL
} else {
RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
},
countRoomAsNotif = true
) {
it.memberships = listOf(Membership.INVITE)
it.roomCategoryFilter = RoomCategoryFilter.ALL
}
addSection(
sections,
activeSpaceAwareQueries,
R.string.bottom_action_rooms,
false,
RoomListViewModel.SpaceFilterStrategy.NORMAL
sections = sections,
activeSpaceUpdaters = activeSpaceAwareQueries,
nameRes = R.string.bottom_action_rooms,
notifyOfLocalEcho = false,
spaceFilterStrategy = if (onlyOrphansInHome) {
RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL
} else {
RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
}
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS
@ -126,10 +137,12 @@ class SpaceRoomListSectionBuilder(
private fun buildRoomsSections(sections: MutableList<RoomsSection>, activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
addSection(
sections, activeSpaceAwareQueries,
R.string.invitations_header,
true,
RoomListViewModel.SpaceFilterStrategy.NOT_IF_ALL
sections = sections,
activeSpaceUpdaters = activeSpaceAwareQueries,
nameRes = R.string.invitations_header,
notifyOfLocalEcho = true,
spaceFilterStrategy = RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL,
countRoomAsNotif = true
) {
it.memberships = listOf(Membership.INVITE)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
@ -140,7 +153,7 @@ class SpaceRoomListSectionBuilder(
activeSpaceAwareQueries,
R.string.bottom_action_favourites,
false,
RoomListViewModel.SpaceFilterStrategy.NOT_IF_ALL
RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
@ -148,11 +161,15 @@ class SpaceRoomListSectionBuilder(
}
addSection(
sections,
activeSpaceAwareQueries,
R.string.bottom_action_rooms,
false,
RoomListViewModel.SpaceFilterStrategy.NORMAL
sections = sections,
activeSpaceUpdaters = activeSpaceAwareQueries,
nameRes = R.string.bottom_action_rooms,
notifyOfLocalEcho = false,
spaceFilterStrategy = if (onlyOrphansInHome) {
RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL
} else {
RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
}
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
@ -160,11 +177,15 @@ class SpaceRoomListSectionBuilder(
}
addSection(
sections,
activeSpaceAwareQueries,
R.string.low_priority_header,
false,
RoomListViewModel.SpaceFilterStrategy.NORMAL
sections = sections,
activeSpaceUpdaters = activeSpaceAwareQueries,
nameRes = R.string.low_priority_header,
notifyOfLocalEcho = false,
spaceFilterStrategy = if (onlyOrphansInHome) {
RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL
} else {
RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
}
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
@ -172,11 +193,15 @@ class SpaceRoomListSectionBuilder(
}
addSection(
sections,
activeSpaceAwareQueries,
R.string.system_alerts_header,
false,
RoomListViewModel.SpaceFilterStrategy.NORMAL
sections = sections,
activeSpaceUpdaters = activeSpaceAwareQueries,
nameRes = R.string.system_alerts_header,
notifyOfLocalEcho = false,
spaceFilterStrategy = if (onlyOrphansInHome) {
RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL
} else {
RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
}
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
@ -228,11 +253,12 @@ class SpaceRoomListSectionBuilder(
}
private fun buildDmSections(sections: MutableList<RoomsSection>, activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
addSection(sections,
activeSpaceAwareQueries,
R.string.invitations_header,
true,
RoomListViewModel.SpaceFilterStrategy.NOT_IF_ALL
addSection(sections = sections,
activeSpaceUpdaters = activeSpaceAwareQueries,
nameRes = R.string.invitations_header,
notifyOfLocalEcho = true,
spaceFilterStrategy = RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL,
countRoomAsNotif = true
) {
it.memberships = listOf(Membership.INVITE)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
@ -242,7 +268,7 @@ class SpaceRoomListSectionBuilder(
activeSpaceAwareQueries,
R.string.bottom_action_favourites,
false,
RoomListViewModel.SpaceFilterStrategy.NOT_IF_ALL
RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
@ -253,7 +279,7 @@ class SpaceRoomListSectionBuilder(
activeSpaceAwareQueries,
R.string.bottom_action_people_x,
false,
RoomListViewModel.SpaceFilterStrategy.NOT_IF_ALL
RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
@ -266,6 +292,7 @@ class SpaceRoomListSectionBuilder(
@StringRes nameRes: Int,
notifyOfLocalEcho: Boolean = false,
spaceFilterStrategy: RoomListViewModel.SpaceFilterStrategy = RoomListViewModel.SpaceFilterStrategy.NONE,
countRoomAsNotif: Boolean = false,
query: (RoomSummaryQueryParams.Builder) -> Unit) {
withQueryParams(
{ query.invoke(it) },
@ -277,7 +304,7 @@ class SpaceRoomListSectionBuilder(
pagedListConfig
).also {
when (spaceFilterStrategy) {
RoomListViewModel.SpaceFilterStrategy.NORMAL -> {
RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> {
activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
override fun updateForSpaceId(roomId: String?) {
it.updateQuery {
@ -288,7 +315,7 @@ class SpaceRoomListSectionBuilder(
}
})
}
RoomListViewModel.SpaceFilterStrategy.NOT_IF_ALL -> {
RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL -> {
activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
override fun updateForSpaceId(roomId: String?) {
if (roomId != null) {
@ -307,7 +334,7 @@ class SpaceRoomListSectionBuilder(
}
})
}
RoomListViewModel.SpaceFilterStrategy.NONE -> {
RoomListViewModel.SpaceFilterStrategy.NONE -> {
// we ignore current space for this one
}
}
@ -320,9 +347,15 @@ class SpaceRoomListSectionBuilder(
.subscribe {
sections.find { it.sectionName == name }
?.notificationCount
?.postValue(session.getNotificationCountForRooms(
roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
))
?.postValue(
if (countRoomAsNotif) {
RoomAggregateNotificationCount(it.size, it.size)
} else {
session.getNotificationCountForRooms(
roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
)
}
)
}.also {
onDisposable.invoke(it)
}
@ -349,14 +382,16 @@ class SpaceRoomListSectionBuilder(
internal fun RoomSummaryQueryParams.process(spaceFilter: RoomListViewModel.SpaceFilterStrategy, currentSpace: String?): RoomSummaryQueryParams {
return when (spaceFilter) {
RoomListViewModel.SpaceFilterStrategy.NORMAL -> {
RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> {
copy(
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(currentSpace)
)
}
RoomListViewModel.SpaceFilterStrategy.NOT_IF_ALL -> {
RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL -> {
if (currentSpace == null) {
this
copy(
activeSpaceFilter = ActiveSpaceFilter.None
)
} else {
copy(
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(currentSpace)

View File

@ -236,10 +236,9 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init
if (initialState.parentSpaceId != null) {
// add it as a child
try {
val via = session.sessionParams.homeServerHost?.let { listOf(it) }.orEmpty()
session.spaceService()
.getSpace(initialState.parentSpaceId)
?.addChildren(roomId, viaServers = via, order = null)
?.addChildren(roomId, viaServers = null, order = null)
} catch (failure: Throwable) {
Timber.w(failure, "Failed to add as a child")
}

View File

@ -151,6 +151,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS"
const val SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE = "SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE"
const val SETTINGS_LABS_SPACES_HOME_AS_ORPHAN = "SETTINGS_LABS_SPACES_HOME_AS_ORPHAN"
private const val SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
@ -956,6 +957,10 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE, false)
}
fun labsSpacesOnlyOrphansInHome(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_SPACES_HOME_AS_ORPHAN, false)
}
/*
* Photo / video picker
*/

View File

@ -17,6 +17,9 @@
package im.vector.app.features.settings
import im.vector.app.R
import im.vector.app.core.preference.VectorSwitchPreference
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import javax.inject.Inject
class VectorSettingsLabsFragment @Inject constructor(
@ -27,6 +30,11 @@ class VectorSettingsLabsFragment @Inject constructor(
override val preferenceXmlRes = R.xml.vector_settings_labs
override fun bindPref() {
// Nothing to do
findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_LABS_SPACES_HOME_AS_ORPHAN)!!.let { pref ->
pref.setOnPreferenceChangeListener { _, _ ->
MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = false))
true
}
}
}
}

View File

@ -31,8 +31,8 @@ import im.vector.app.space
import org.matrix.android.sdk.api.session.group.model.GroupSummary
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.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
@ -43,6 +43,8 @@ class SpaceSummaryController @Inject constructor(
var callback: Callback? = null
private var viewState: SpaceListViewState? = null
private val subSpaceComparator: Comparator<SpaceChildInfo> = compareBy<SpaceChildInfo> { it.order }.thenBy { it.childRoomId }
init {
requestModelBuild()
}
@ -132,13 +134,13 @@ class SpaceSummaryController @Inject constructor(
}
rootSpaces
?.sortedBy { it.displayName }
?.sortedBy { it.roomId }
?.forEach { groupSummary ->
val isSelected = selected is RoomGroupingMethod.BySpace && groupSummary.roomId == selected.space()?.roomId
// does it have children?
val subSpaces = groupSummary.spaceChildren?.filter { childInfo ->
summaries?.indexOfFirst { it.roomId == childInfo.childRoomId } != -1
}
}?.sortedWith(subSpaceComparator)
val hasChildren = (subSpaces?.size ?: 0) > 0
val expanded = expandedStates[groupSummary.roomId] == true
@ -163,24 +165,7 @@ class SpaceSummaryController @Inject constructor(
if (hasChildren && expanded) {
// it's expanded
subSpaces?.forEach { child ->
summaries?.firstOrNull { it.roomId == child.childRoomId }?.let { childSum ->
val isChildSelected = selected is RoomGroupingMethod.BySpace && childSum.roomId == selected.space()?.roomId
spaceSummaryItem {
avatarRenderer(avatarRenderer)
id(child.childRoomId)
hasChildren(false)
selected(isChildSelected)
matrixItem(MatrixItem.RoomItem(child.childRoomId, child.name, child.avatarUrl))
listener { callback?.onSpaceSelected(childSum) }
indent(1)
countState(
UnreadCounterBadgeView.State(
groupSummary.notificationCount,
groupSummary.highlightCount > 0
)
)
}
}
buildSubSpace(summaries, expandedStates, selected, child, 1, 3)
}
}
}
@ -191,6 +176,45 @@ class SpaceSummaryController @Inject constructor(
}
}
private fun buildSubSpace(summaries: List<RoomSummary>?,
expandedStates: Map<String, Boolean>,
selected: RoomGroupingMethod,
info: SpaceChildInfo, currentDepth: Int, maxDepth: Int) {
if (currentDepth >= maxDepth) return
val childSummary = summaries?.firstOrNull { it.roomId == info.childRoomId } ?: return
// does it have children?
val subSpaces = childSummary.spaceChildren?.filter { childInfo ->
summaries.indexOfFirst { it.roomId == childInfo.childRoomId } != -1
}?.sortedWith(subSpaceComparator)
val expanded = expandedStates[childSummary.roomId] == true
val isSelected = selected is RoomGroupingMethod.BySpace && childSummary.roomId == selected.space()?.roomId
subSpaceSummaryItem {
avatarRenderer(avatarRenderer)
id(childSummary.roomId)
hasChildren(!subSpaces.isNullOrEmpty())
selected(isSelected)
expanded(expanded)
onMore { callback?.onSpaceSettings(childSummary) }
matrixItem(childSummary.toMatrixItem())
listener { callback?.onSpaceSelected(childSummary) }
toggleExpand { callback?.onToggleExpand(childSummary) }
indent(currentDepth)
countState(
UnreadCounterBadgeView.State(
childSummary.notificationCount,
childSummary.highlightCount > 0
)
)
}
if (expanded) {
subSpaces?.forEach {
buildSubSpace(summaries, expandedStates, selected, it, currentDepth + 1, maxDepth)
}
}
}
interface Callback {
fun onSpaceSelected(spaceSummary: RoomSummary?)
fun onSpaceInviteSelected(spaceSummary: RoomSummary)

View File

@ -16,6 +16,7 @@
package im.vector.app.features.spaces
import android.view.View
import android.widget.ImageView
import android.widget.Space
import android.widget.TextView
@ -37,17 +38,18 @@ import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_space)
abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null
@EpoxyAttribute var onMore: (() -> Unit)? = null
@EpoxyAttribute var toggleExpand: (() -> Unit)? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: (() -> Unit)? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onMore: (() -> Unit)? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var toggleExpand: (() -> Unit)? = null
@EpoxyAttribute var expanded: Boolean = false
@EpoxyAttribute var hasChildren: Boolean = false
@EpoxyAttribute var indent: Int = 0
@EpoxyAttribute var countState : UnreadCounterBadgeView.State = UnreadCounterBadgeView.State(0, false)
@EpoxyAttribute var description: String? = null
@EpoxyAttribute var showSeparator: Boolean = false
override fun bind(holder: Holder) {
super.bind(holder)
@ -83,6 +85,7 @@ abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
}
holder.indentSpace.isVisible = indent > 0
holder.separator.isVisible = showSeparator
avatarRenderer.renderSpace(matrixItem, holder.avatarImageView)
holder.counterBadgeView.render(countState)
@ -102,5 +105,6 @@ abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
val collapseIndicator by bind<ImageView>(R.id.groupChildrenCollapse)
val indentSpace by bind<Space>(R.id.indent)
val counterBadgeView by bind<UnreadCounterBadgeView>(R.id.groupCounterBadge)
val separator by bind<View>(R.id.groupBottomSeparator)
}
}

View File

@ -28,8 +28,7 @@ import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.RoomGroupingMethod
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.ui.UiStateRepository
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.group
import im.vector.app.space
import io.reactivex.Observable
@ -45,6 +44,7 @@ import org.matrix.android.sdk.api.session.room.RoomSortOrder
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.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.rx.asObservable
@ -54,8 +54,7 @@ import java.util.concurrent.TimeUnit
class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: SpaceListViewState,
private val appStateHandler: AppStateHandler,
private val session: Session,
private val stringProvider: StringProvider,
private val uiStateRepository: UiStateRepository
private val vectorPreferences: VectorPreferences
) : VectorViewModel<SpaceListViewState, SpaceListAction, SpaceListViewEvents>(initialState) {
@AssistedFactory
@ -108,21 +107,34 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
}
}.disposeOnClear()
// XXX there should be a way to refactor this and share it
session.getPagedRoomSummariesLive(
roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf {
vectorPreferences.labsSpacesOnlyOrphansInHome()
} ?: ActiveSpaceFilter.None
}, sortOrder = RoomSortOrder.NONE
).asObservable()
.throttleFirst(300, TimeUnit.MILLISECONDS)
.observeOn(Schedulers.computation())
.subscribe {
val counts = session.getNotificationCountForRooms(
val inviteCount = session.getRoomSummaries(roomSummaryQueryParams {
this.memberships = listOf(Membership.INVITE)
}).size
val totalCount = session.getNotificationCountForRooms(
roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf {
vectorPreferences.labsSpacesOnlyOrphansInHome()
} ?: ActiveSpaceFilter.None
}
)
val counts = RoomAggregateNotificationCount(
totalCount.notificationCount + inviteCount,
totalCount.highlightCount + inviteCount
)
setState {
copy(
homeAggregateCount = counts
@ -217,7 +229,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
.rx()
.liveUser(session.myUserId)
.map {
it.getOrNull()
it.getOrNull()
},
session
.rx()

View File

@ -0,0 +1,102 @@
/*
* 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.widget.ImageView
import android.widget.Space
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
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.platform.CheckableConstraintLayout
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_sub_space)
abstract class SubSpaceSummaryItem : VectorEpoxyModel<SubSpaceSummaryItem.Holder>() {
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: (() -> Unit)? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onMore: (() -> Unit)? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var toggleExpand: (() -> Unit)? = null
@EpoxyAttribute var expanded: Boolean = false
@EpoxyAttribute var hasChildren: Boolean = false
@EpoxyAttribute var indent: Int = 0
@EpoxyAttribute var countState: UnreadCounterBadgeView.State = UnreadCounterBadgeView.State(0, false)
override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.setOnClickListener { listener?.invoke() }
holder.groupNameView.text = matrixItem.displayName
holder.rootView.isChecked = selected
if (onMore != null) {
holder.moreView.isVisible = true
holder.moreView.setOnClickListener(
DebouncedClickListener({ _ ->
onMore?.invoke()
})
)
} else {
holder.moreView.isVisible = false
}
holder.collapseIndicator.setImageDrawable(
ContextCompat.getDrawable(holder.view.context,
if (expanded) R.drawable.ic_expand_less else R.drawable.ic_expand_more
)
)
holder.collapseIndicator.setOnClickListener(
DebouncedClickListener({ _ ->
toggleExpand?.invoke()
})
)
holder.collapseIndicator.isVisible = hasChildren
holder.indentSpace.isVisible = indent > 0
holder.indentSpace.updateLayoutParams {
width = indent * 30
}
avatarRenderer.renderSpace(matrixItem, holder.avatarImageView)
holder.counterBadgeView.render(countState)
}
override fun unbind(holder: Holder) {
avatarRenderer.clear(holder.avatarImageView)
super.unbind(holder)
}
class Holder : VectorEpoxyHolder() {
val avatarImageView by bind<ImageView>(R.id.groupAvatarImageView)
val groupNameView by bind<TextView>(R.id.groupNameView)
val rootView by bind<CheckableConstraintLayout>(R.id.itemGroupLayout)
val moreView by bind<ImageView>(R.id.groupTmpLeave)
val collapseIndicator by bind<ImageView>(R.id.groupChildrenCollapse)
val indentSpace by bind<Space>(R.id.indent)
val counterBadgeView by bind<UnreadCounterBadgeView>(R.id.groupCounterBadge)
}
}

View File

@ -18,13 +18,21 @@ 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 im.vector.app.R
import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.genericPillItem
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.list.spaceChildInfoItem
import me.gujun.android.span.span
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError.Companion.M_UNRECOGNIZED
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
@ -32,13 +40,16 @@ import javax.inject.Inject
class SpaceDirectoryController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val errorFormatter: ErrorFormatter
) : TypedEpoxyController<SpaceDirectoryState>() {
interface InteractionListener {
fun onButtonClick(spaceChildInfo: SpaceChildInfo)
fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo)
fun onRoomClick(spaceChildInfo: SpaceChildInfo)
fun retry()
}
var listener: InteractionListener? = null
@ -50,6 +61,33 @@ class SpaceDirectoryController @Inject constructor(
loadingItem {
id("loading")
}
} else if (results is Fail) {
val failure = results.error
if (failure is Failure.ServerError && failure.error.code == M_UNRECOGNIZED) {
genericPillItem {
id("HS no Support")
imageRes(R.drawable.error)
tintIcon(false)
text(
span {
span(stringProvider.getString(R.string.spaces_no_server_support_title)) {
textStyle = "bold"
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)
}
+"\n\n"
span(stringProvider.getString(R.string.spaces_no_server_support_description)) {
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
}
}
)
}
} else {
errorWithRetryItem {
id("api_err")
text(errorFormatter.toHumanReadable(failure))
listener { listener?.retry() }
}
}
} else {
val flattenChildInfo = results?.invoke()
?.filter {

View File

@ -94,6 +94,9 @@ class SpaceDirectoryFragment @Inject constructor(
return true
}
override fun retry() {
viewModel.handle(SpaceDirectoryViewAction.Retry)
}
// override fun navigateToRoom(roomId: String) {
// viewModel.handle(SpaceDirectoryViewAction.NavigateToRoom(roomId))
// }

View File

@ -25,4 +25,5 @@ sealed class SpaceDirectoryViewAction : VectorViewModelAction {
data class ShowDetails(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction()
data class NavigateToRoom(val roomId: String) : SpaceDirectoryViewAction()
object HandleBack : SpaceDirectoryViewAction()
object Retry : SpaceDirectoryViewAction()
}

View File

@ -39,7 +39,7 @@ import org.matrix.android.sdk.rx.rx
import timber.log.Timber
class SpaceDirectoryViewModel @AssistedInject constructor(
@Assisted initialState: SpaceDirectoryState,
@Assisted val initialState: SpaceDirectoryState,
private val session: Session
) : VectorViewModel<SpaceDirectoryState, SpaceDirectoryViewAction, SpaceDirectoryViewEvents>(initialState) {
@ -63,11 +63,21 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
val spaceSum = session.getRoomSummary(initialState.spaceId)
setState {
copy(
childList = spaceSum?.spaceChildren ?: emptyList(),
spaceSummaryApiResult = Loading()
childList = spaceSum?.spaceChildren ?: emptyList()
)
}
refreshFromApi()
observeJoinedRooms()
observeMembershipChanges()
}
private fun refreshFromApi() {
setState {
copy(
spaceSummaryApiResult = Loading()
)
}
viewModelScope.launch(Dispatchers.IO) {
try {
val query = session.spaceService().querySpaceChildren(initialState.spaceId)
@ -84,8 +94,6 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
}
}
}
observeJoinedRooms()
observeMembershipChanges()
}
private fun observeJoinedRooms() {
@ -139,13 +147,16 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
is SpaceDirectoryViewAction.NavigateToRoom -> {
_viewEvents.post(SpaceDirectoryViewEvents.NavigateToRoom(action.roomId))
}
is SpaceDirectoryViewAction.ShowDetails -> {
is SpaceDirectoryViewAction.ShowDetails -> {
// This is temporary for now to at least display something for the space beta
// It's not ideal as it's doing some peeking that is not needed.
session.permalinkService().createRoomPermalink(action.spaceChildInfo.childRoomId)?.let {
_viewEvents.post(SpaceDirectoryViewEvents.NavigateToMxToBottomSheet(it))
_viewEvents.post(SpaceDirectoryViewEvents.NavigateToMxToBottomSheet(it))
}
}
SpaceDirectoryViewAction.Retry -> {
refreshFromApi()
}
}
}

View File

@ -19,6 +19,8 @@ package im.vector.app.features.spaces.manage
import androidx.recyclerview.widget.DiffUtil
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController
import im.vector.app.R
import im.vector.app.core.ui.list.GenericPillItem_
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.core.utils.createUIHandler
import im.vector.app.features.home.AvatarRenderer
@ -56,6 +58,8 @@ class AddRoomListController @Inject constructor(
var listener: Listener? = null
var ignoreRooms: List<String>? = null
var subHeaderText: CharSequence? = null
var initialLoadOccurred = false
fun boundaryChange(boundary: ResultBoundaries) {
@ -100,6 +104,15 @@ class AddRoomListController @Inject constructor(
expanded(true)
}
)
if (subHeaderText != null) {
add(
GenericPillItem_().apply {
id("sub_header")
text(subHeaderText)
imageRes(R.drawable.ic_info)
}
)
}
}
super.addModels(filteredModel)
if (!initialLoadOccurred) {

View File

@ -16,9 +16,6 @@
package im.vector.app.features.spaces.manage
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
@ -26,12 +23,8 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyViewHolder
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel
@ -41,7 +34,6 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentSpaceAddRoomsBinding
import im.vector.app.features.home.room.list.RoomCategoryItem_
import io.reactivex.rxkotlin.subscribeBy
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import java.util.concurrent.TimeUnit
@ -88,6 +80,7 @@ class SpaceAddRoomFragment @Inject constructor(
}
.disposeOnDestroyView()
spaceEpoxyController.subHeaderText = getString(R.string.spaces_feeling_experimental_subspace)
viewModel.selectionListLiveData.observe(viewLifecycleOwner) {
spaceEpoxyController.selectedItems = it
roomEpoxyController.selectedItems = it
@ -183,36 +176,6 @@ class SpaceAddRoomFragment @Inject constructor(
}
views.roomList.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
views.roomList.addItemDecoration(
object : DividerItemDecoration(context, VERTICAL) {
val decorationDrawable = ContextCompat.getDrawable(requireContext(), R.drawable.divider_horizontal)
override fun getDrawable(): Drawable? {
return decorationDrawable
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val position = parent.getChildAdapterPosition(view)
val vh = parent.findViewHolderForAdapterPosition(position)
val nextIsSectionOrFinal = parent.findViewHolderForAdapterPosition(position + 1)?.let {
(it as? EpoxyViewHolder)?.model is RoomCategoryItem_
} ?: true
if (vh == null
|| (vh as? EpoxyViewHolder)?.model is RoomCategoryItem_
|| nextIsSectionOrFinal
) {
outRect.setEmpty()
} else {
super.getItemOffsets(outRect, view, parent, state)
}
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
}
}
)
views.roomList.setHasFixedSize(true)
concatAdapter.addAdapter(roomEpoxyController.adapter)

View File

@ -174,7 +174,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
try {
session.spaceService().getSpace(initialState.spaceId)!!.addChildren(
roomId = roomId,
viaServers = listOf(session.sessionParams.homeServerHost ?: ""),
viaServers = null,
order = null
)
completed.add(roomId)

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="M14.6666,7.9999C14.6666,11.6818 11.6818,14.6666 7.9999,14.6666C4.318,14.6666 1.3333,11.6818 1.3333,7.9999C1.3333,4.318 4.318,1.3333 7.9999,1.3333C11.6818,1.3333 14.6666,4.318 14.6666,7.9999ZM6.3333,7.9999C6.3333,8.4353 6.6115,8.8057 6.9999,8.943V10.9999C6.9999,11.5522 7.4476,11.9999 7.9999,11.9999H8.9999C9.5522,11.9999 9.9999,11.5522 9.9999,10.9999C9.9999,10.4476 9.5522,9.9999 8.9999,9.9999V7.9999C8.9999,7.4476 8.5522,6.9999 7.9999,6.9999H7.3333C6.781,6.9999 6.3333,7.4476 6.3333,7.9999ZM7.9999,6.6666C8.5522,6.6666 8.9999,6.2189 8.9999,5.6666C8.9999,5.1143 8.5522,4.6666 7.9999,4.6666C7.4476,4.6666 6.9999,5.1143 6.9999,5.6666C6.9999,6.2189 7.4476,6.6666 7.9999,6.6666Z"
android:fillColor="#737D8C"
android:fillType="evenOdd"/>
</vector>

View File

@ -10,7 +10,7 @@
android:id="@+id/roomList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_header_panel_background"
android:background="?riotx_background"
android:overScrollMode="always"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_room_to_add_in_space" />

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:background="@drawable/rounded_rect_shape_8"
android:backgroundTint="?riotx_reaction_background_off"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:id="@+id/itemGenericPillImage"
android:layout_width="20dp"
android:layout_height="20dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_info"
app:tint="?riotx_text_secondary" />
<TextView
android:id="@+id/itemGenericPillText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:gravity="start"
tools:text="@tools:sample/lorem" />
</LinearLayout>

View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<im.vector.app.core.platform.CheckableConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemGroupLayout"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/bg_space_item"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<Space
android:id="@+id/indent"
android:layout_width="20dp"
android:layout_height="match_parent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ImageView
android:id="@+id/groupAvatarImageView"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_gravity="center"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:contentDescription="@string/avatar"
android:duplicateParentState="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/indent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<im.vector.app.features.home.room.list.UnreadCounterBadgeView
android:id="@+id/groupCounterBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:gravity="center"
android:minWidth="16dp"
android:minHeight="16dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textColor="@android:color/white"
android:textSize="10sp"
android:visibility="gone"
app:layout_constraintCircle="@+id/groupAvatarImageView"
app:layout_constraintCircleAngle="45"
app:layout_constraintCircleRadius="14dp"
tools:background="@drawable/bg_unread_highlight"
tools:text="147"
tools:visibility="visible" />
<TextView
android:id="@+id/groupNameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/groupChildrenCollapse"
app:layout_constraintStart_toEndOf="@+id/groupAvatarImageView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/lorem/random" />
<ImageView
android:id="@+id/groupChildrenCollapse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="?selectableItemBackground"
android:clickable="true"
android:importantForAccessibility="no"
android:src="@drawable/ic_expand_less_white"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/groupTmpLeave"
app:layout_constraintTop_toTopOf="parent"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix"
tools:src="@drawable/ic_expand_more_white"
tools:visibility="visible" />
<ImageView
android:id="@+id/groupTmpLeave"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginEnd="4dp"
android:background="?selectableItemBackground"
android:clickable="true"
android:importantForAccessibility="no"
android:padding="4dp"
android:src="@drawable/ic_more_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/groupAvatarChevron"
app:layout_constraintTop_toTopOf="parent"
app:tint="?riotx_text_secondary" />
<ImageView
android:id="@+id/groupAvatarChevron"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="21dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_arrow_right"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix" />
<!-- <View-->
<!-- android:id="@+id/groupBottomSeparator"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="1dp"-->
<!-- android:background="?riotx_header_panel_border_mobile"-->
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="parent" />-->
</im.vector.app.core.platform.CheckableConstraintLayout>

View File

@ -3362,4 +3362,10 @@
<string name="space_mark_as_suggested">Mark as suggested</string>
<string name="space_mark_as_not_suggested">Mark as not suggested</string>
<string name="space_manage_rooms_and_spaces">Manage rooms and spaces</string>
<string name="labs_space_show_orphan_in_home">Experimental Space - Only show orphans in Home</string>
<string name="spaces_feeling_experimental_subspace">Feeling experimental?\nYou can add existing spaces to a space.</string>
<string name="spaces_no_server_support_title">It looks like your homeserver does not support Spaces yet</string>
<string name="spaces_no_server_support_description">Please contact your homserver admin for further information</string>
</resources>

View File

@ -52,4 +52,9 @@
android:summary="@string/labs_use_restricted_join_rule_desc"/>
<!--</im.vector.app.core.preference.VectorPreferenceCategory>-->
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_LABS_SPACES_HOME_AS_ORPHAN"
android:title="@string/labs_space_show_orphan_in_home"/>
</androidx.preference.PreferenceScreen>