Space first commit

This commit is contained in:
Valere 2020-12-29 17:34:25 +01:00
parent 8b5d86f68d
commit c5fa0a413f
60 changed files with 1523 additions and 59 deletions

View File

@ -39,6 +39,8 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.api.session.sync.SyncState
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.session.widgets.model.Widget
@ -66,6 +68,13 @@ class RxSession(private val session: Session) {
}
}
fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Observable<List<SpaceSummary>> {
return session.spaceService().getSpaceSummariesLive(queryParams).asObservable()
.startWithCallable {
session.spaceService().getSpaceSummaries(queryParams)
}
}
fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
return session.getBreadcrumbsLive(queryParams).asObservable()
.startWithCallable {

View File

@ -48,6 +48,7 @@ import org.matrix.android.sdk.api.session.search.SearchService
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.signout.SignOutService
import org.matrix.android.sdk.api.session.space.SpaceService
import org.matrix.android.sdk.api.session.sync.FilterService
import org.matrix.android.sdk.api.session.sync.SyncState
import org.matrix.android.sdk.api.session.terms.TermsService
@ -227,6 +228,11 @@ interface Session :
*/
fun thirdPartyService(): ThirdPartyService
/**
* Returns the space service associated with the session
*/
fun spaceService(): SpaceService
/**
* Add a listener to the session.
* @param listener the listener to add.

View File

@ -51,6 +51,7 @@ object EventType {
const val STATE_ROOM_JOIN_RULES = "m.room.join_rules"
const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access"
const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
const val STATE_SPACE_CHILD = "m.space.child"
/**
* Note that this Event has been deprecated, see

View File

@ -20,6 +20,7 @@ import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.query.RoomTagQueryFilter
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomType
fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams {
return RoomSummaryQueryParams.Builder().apply(init).build()
@ -36,6 +37,7 @@ data class RoomSummaryQueryParams(
val memberships: List<Membership>,
val roomCategoryFilter: RoomCategoryFilter?,
val roomTagQueryFilter: RoomTagQueryFilter?
val excludeType: List<String?>
) {
class Builder {
@ -46,6 +48,7 @@ data class RoomSummaryQueryParams(
var memberships: List<Membership> = Membership.all()
var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL
var roomTagQueryFilter: RoomTagQueryFilter? = null
var excludeType: List<String?> = listOf(RoomType.SPACE)
fun build() = RoomSummaryQueryParams(
roomId = roomId,
@ -54,6 +57,7 @@ data class RoomSummaryQueryParams(
memberships = memberships,
roomCategoryFilter = roomCategoryFilter,
roomTagQueryFilter = roomTagQueryFilter
excludeType = excludeType
)
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.model
interface IRoomSummary {
val roomId: String
val displayName: String
val name: String
val topic: String
val avatarUrl: String
val canonicalAlias: String?
val aliases: List<String>
val joinedMembersCount: Int?
val invitedMembersCount: Int?
val otherMemberIds: List<String>
val roomType: String?
}

View File

@ -27,19 +27,19 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
* It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService]
*/
data class RoomSummary constructor(
val roomId: String,
override val roomId: String,
// Computed display name
val displayName: String = "",
val name: String = "",
val topic: String = "",
val avatarUrl: String = "",
val canonicalAlias: String? = null,
val aliases: List<String> = emptyList(),
val isDirect: Boolean = false,
val joinedMembersCount: Int? = 0,
val invitedMembersCount: Int? = 0,
override val displayName: String = "",
override val name: String = "",
override val topic: String = "",
override val avatarUrl: String = "",
override val canonicalAlias: String? = null,
override val aliases: List<String> = emptyList(),
override val joinedMembersCount: Int? = 0,
override val invitedMembersCount: Int? = 0,
val latestPreviewableEvent: TimelineEvent? = null,
val otherMemberIds: List<String> = emptyList(),
override val otherMemberIds: List<String> = emptyList(),
val isDirect: Boolean = false,
val notificationCount: Int = 0,
val highlightCount: Int = 0,
val hasUnreadMessages: Boolean = false,
@ -54,8 +54,9 @@ data class RoomSummary constructor(
val inviterId: String? = null,
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null,
val hasFailedSending: Boolean = false
) {
val hasFailedSending: Boolean = false,
override val roomType: String? = null
) : IRoomSummary {
val isVersioned: Boolean
get() = versioningState != VersioningState.NONE

View File

@ -0,0 +1,23 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.model
object RoomType {
const val SPACE = "m.space"
const val MESSAGING = "m.messaging"
}

View File

@ -21,10 +21,11 @@ import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
// TODO Give a way to include other initial states
class CreateRoomParams {
open class CreateRoomParams {
/**
* A public visibility indicates that the room will be shown in the published room list.
* A private visibility will hide the room from the published room list.
@ -111,6 +112,17 @@ class CreateRoomParams {
}
}
var roomType: String? = RoomType.MESSAGING
set(value) {
field = value
if (value != null) {
creationContent[CREATION_CONTENT_KEY_ROOM_TYPE] = value
} else {
// This is the default value, we remove the field
creationContent.remove(CREATION_CONTENT_KEY_ROOM_TYPE)
}
}
/**
* The power level content to override in the default power level event
*/
@ -138,5 +150,6 @@ class CreateRoomParams {
companion object {
private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"
private const val CREATION_CONTENT_KEY_ROOM_TYPE = "type"
}
}

View File

@ -26,5 +26,7 @@ import com.squareup.moshi.JsonClass
data class RoomCreateContent(
@Json(name = "creator") val creator: String? = null,
@Json(name = "room_version") val roomVersion: String? = null,
@Json(name = "predecessor") val predecessor: Predecessor? = null
@Json(name = "predecessor") val predecessor: Predecessor? = null,
// Defines the room type, see #RoomType (user extensible)
@Json(name = "type") val type: String? = null
)

View File

@ -0,0 +1,27 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.space
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
class CreateSpaceParams : CreateRoomParams() {
init {
roomType = RoomType.SPACE
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.space
import org.matrix.android.sdk.api.session.room.Room
interface Space {
fun asRoom() : Room
suspend fun addRoom(roomId: String)
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.space
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
typealias SpaceSummaryQueryParams = RoomSummaryQueryParams
interface SpaceService {
/**
* Create a room asynchronously
*/
suspend fun createSpace(params: CreateSpaceParams): String
/**
* Get a space from a roomId
* @param roomId the roomId to look for.
* @return a room with roomId or null if room type is not space
*/
fun getSpace(spaceId: String): Space?
/**
* Get a live list of space summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of List[SpaceSummary]
*/
fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<SpaceSummary>>
fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<SpaceSummary>
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.space
import org.matrix.android.sdk.api.session.room.model.IRoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class SpaceSummary(
val spaceId: String,
val roomSummary: RoomSummary,
val children: List<IRoomSummary>
) : IRoomSummary by roomSummary

View File

@ -0,0 +1,52 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.space.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* "content": {
* "via": ["example.com"],
* "present": true,
* "order": "abcd",
* "default": true
* }
*/
@JsonClass(generateAdapter = true)
data class SpaceChildContent(
/**
* Key which gives a list of candidate servers that can be used to join the room
*/
@Json(name = "via") val via: List<String>? = null,
/**
* present: true key is included to distinguish from a deleted state event
*/
@Json(name = "present") val present: Boolean? = false,
/**
* The order key is a string which is used to provide a default ordering of siblings in the room list.
* (Rooms are sorted based on a lexicographic ordering of order values; rooms with no order come last.
* orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7F (~),
* or consist of more than 50 characters, are forbidden and should be ignored if received.)
*/
@Json(name = "order") val order: String? = null,
/**
* The default flag on a child listing allows a space admin to list the "default" sub-spaces and rooms in that space.
* This means that when a user joins the parent space, they will automatically be joined to those default children.
*/
@Json(name = "default") val default: Boolean? = true
)

View File

@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.user.model.User
import java.util.Locale
@ -86,9 +87,9 @@ sealed class MatrixItem(
}
protected fun checkId() {
if (!id.startsWith(getIdPrefix())) {
error("Wrong usage of MatrixItem: check the id $id should start with ${getIdPrefix()}")
}
// if (!id.startsWith(getIdPrefix())) {
// error("Wrong usage of MatrixItem: check the id $id should start with ${getIdPrefix()}")
// }
}
/**
@ -151,6 +152,8 @@ fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatar
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
fun SpaceSummary.toMatrixItem() = MatrixItem.RoomItem(spaceId, displayName, avatarUrl)
// If no name is available, use room alias as Riot-Web does
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)

View File

@ -29,15 +29,17 @@ import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityField
import org.matrix.android.sdk.internal.database.model.RoomEntityFields
import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields
import timber.log.Timber
import javax.inject.Inject
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
companion object {
const val SESSION_STORE_SCHEMA_VERSION = 9L
const val SESSION_STORE_SCHEMA_VERSION = 10L
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@ -52,6 +54,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
if (oldVersion <= 6) migrateTo7(realm)
if (oldVersion <= 7) migrateTo8(realm)
if (oldVersion <= 8) migrateTo9(realm)
if (oldVersion <= 9) migrateTo10(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
@ -194,4 +197,19 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
}
}
}
fun migrateTo10(realm: DynamicRealm) {
Timber.d("Step 9 -> 10")
realm.schema.get("RoomSummaryEntity")
?.addField(RoomSummaryEntityFields.ROOM_TYPE, String::class.java)
?.transform { obj ->
// Should I put messaging type here?
obj.setString(RoomSummaryEntityFields.ROOM_TYPE, null)
}
realm.schema.create("SpaceSummaryEntity")
?.addField(SpaceSummaryEntityFields.SPACE_ID, String::class.java, FieldAttribute.PRIMARY_KEY)
?.addRealmObjectField(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
?.addRealmListField(SpaceSummaryEntityFields.CHILDREN.`$`, realm.schema.get("RoomSummaryEntity")!!)
}
}

View File

@ -63,7 +63,8 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel,
inviterId = roomSummaryEntity.inviterId,
hasFailedSending = roomSummaryEntity.hasFailedSending
hasFailedSending = roomSummaryEntity.hasFailedSending,
roomType = roomSummaryEntity.roomType
)
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.mapper
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity
import javax.inject.Inject
internal class SpaceSummaryMapper @Inject constructor(private val roomSummaryMapper: RoomSummaryMapper) {
fun map(spaceSummaryEntity: SpaceSummaryEntity): SpaceSummary {
return SpaceSummary(
spaceId = spaceSummaryEntity.spaceId,
roomSummary = roomSummaryMapper.map(spaceSummaryEntity.roomSummaryEntity!!),
children = spaceSummaryEntity.children.map {
roomSummaryMapper.map(it)
}
)
}
}

View File

@ -61,6 +61,7 @@ import io.realm.annotations.RealmModule
CurrentStateEventEntity::class,
UserAccountDataEntity::class,
ScalarTokenEntity::class,
WellknownIntegrationManagerConfigEntity::class
WellknownIntegrationManagerConfigEntity::class,
SpaceSummaryEntity::class
])
internal class SessionRealmModule

View File

@ -0,0 +1,41 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.model
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class SpaceSummaryEntity(@PrimaryKey var spaceId: String = "",
var roomSummaryEntity: RoomSummaryEntity? = null,
var children: RealmList<RoomSummaryEntity> = RealmList()
// TODO public / private .. and more
) : RealmObject() {
// Do we want to denormalize that ?
// private var membershipStr: String = Membership.NONE.name
// var membership: Membership
// get() {
// return Membership.valueOf(membershipStr)
// }
// set(value) {
// membershipStr = value.name
// }
companion object
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.query
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.createObject
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields
internal fun SpaceSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<SpaceSummaryEntity> {
val query = realm.where<SpaceSummaryEntity>()
query.isNotNull(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`)
if (roomId != null) {
query.equalTo(SpaceSummaryEntityFields.SPACE_ID, roomId)
}
query.sort(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.DISPLAY_NAME)
return query
}
internal fun SpaceSummaryEntity.Companion.findByAlias(realm: Realm, roomAlias: String): SpaceSummaryEntity? {
val spaceSummary = realm.where<SpaceSummaryEntity>()
.isNotNull(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`)
.equalTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.CANONICAL_ALIAS, roomAlias)
.findFirst()
if (spaceSummary != null) {
return spaceSummary
}
return realm.where<SpaceSummaryEntity>()
.isNotNull(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`)
.contains(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.FLAT_ALIASES, "|$roomAlias")
.findFirst()
}
internal fun SpaceSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): SpaceSummaryEntity {
return where(realm, roomId).findFirst() ?: realm.createObject<SpaceSummaryEntity>(roomId).also {
it.roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
}
}

View File

@ -49,6 +49,7 @@ import org.matrix.android.sdk.api.session.search.SearchService
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.signout.SignOutService
import org.matrix.android.sdk.api.session.space.SpaceService
import org.matrix.android.sdk.api.session.sync.FilterService
import org.matrix.android.sdk.api.session.terms.TermsService
import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService
@ -121,7 +122,8 @@ internal class DefaultSession @Inject constructor(
private val thirdPartyService: Lazy<ThirdPartyService>,
private val callSignalingService: Lazy<CallSignalingService>,
@UnauthenticatedWithCertificate
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>,
private val spaceService: Lazy<SpaceService>
) : Session,
RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(),
@ -265,6 +267,8 @@ internal class DefaultSession @Inject constructor(
override fun thirdPartyService(): ThirdPartyService = thirdPartyService.get()
override fun spaceService(): SpaceService = spaceService.get()
override fun getOkHttpClient(): OkHttpClient {
return unauthenticatedWithCertificateOkHttpClient.get()
}

View File

@ -24,6 +24,7 @@ import org.commonmark.renderer.html.HtmlRenderer
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
import org.matrix.android.sdk.api.session.room.RoomService
import org.matrix.android.sdk.api.session.space.SpaceService
import org.matrix.android.sdk.internal.session.DefaultFileService
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.directory.DirectoryAPI
@ -89,6 +90,7 @@ import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask
import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask
import org.matrix.android.sdk.internal.session.room.uploads.DefaultGetUploadsTask
import org.matrix.android.sdk.internal.session.room.uploads.GetUploadsTask
import org.matrix.android.sdk.internal.session.space.DefaultSpaceService
import retrofit2.Retrofit
@Module
@ -135,6 +137,9 @@ internal abstract class RoomModule {
@Binds
abstract fun bindRoomService(service: DefaultRoomService): RoomService
@Binds
abstract fun bindSpaceService(service: DefaultSpaceService): SpaceService
@Binds
abstract fun bindRoomDirectoryService(service: DefaultRoomDirectoryService): RoomDirectoryService

View File

@ -0,0 +1,50 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.relationship
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.query.whereType
/**
* Relationship between rooms and spaces
* The intention is that rooms and spaces form a hierarchy, which clients can use to structure the user's room list into a tree view.
* The parent/child relationship can be expressed in one of two ways:
* - The admins of a space can advertise rooms and subspaces for their space by setting m.space.child state events.
* The state_key is the ID of a child room or space, and the content should contain a via key which gives
* a list of candidate servers that can be used to join the room. present: true key is included to distinguish from a deleted state event.
*
* - Separately, rooms can claim parents via the m.room.parent state event:
*/
internal class RoomRelationshipHelper(private val realm: Realm,
private val roomId: String
) {
fun getDirectChildrenDescriptions(): List<String> {
return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD)
.findAll()
.filter { ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()?.present == true }
.mapNotNull {
// ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()
it.roomId
}
}
}

View File

@ -189,6 +189,10 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
query.equalTo(RoomSummaryEntityFields.IS_SERVER_NOTICE, sn)
}
}
queryParams.excludeType.forEach {
query.notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, it)
}
return query
}
}

View File

@ -25,6 +25,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomAliasesContent
import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
@ -36,6 +38,7 @@ import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates
import org.matrix.android.sdk.internal.database.query.getOrCreate
@ -46,6 +49,7 @@ import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver
import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.room.relationship.RoomRelationshipHelper
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications
import timber.log.Timber
@ -89,6 +93,10 @@ internal class RoomSummaryUpdater @Inject constructor(
val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root
val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root
val lastAliasesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root
val roomCreateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CREATE, stateKey = "")?.root
val roomType = ContentMapper.map(roomCreateEvent?.content).toModel<RoomCreateContent>()?.type
roomSummaryEntity.roomType = roomType
// Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
@ -152,6 +160,16 @@ internal class RoomSummaryUpdater @Inject constructor(
crossSigningService.onUsersDeviceUpdate(otherRoomMembers)
}
}
if (roomType == RoomType.SPACE) {
val spaceSummaryEntity = SpaceSummaryEntity.getOrCreate(realm, roomId)
spaceSummaryEntity.roomSummaryEntity = roomSummaryEntity
spaceSummaryEntity.children.clear()
spaceSummaryEntity.children.addAll(
RoomRelationshipHelper(realm, roomId).getDirectChildrenDescriptions()
.map { RoomSummaryEntity.getOrCreate(realm, roomId) }
)
}
}
private fun RoomSummaryEntity.updateHasFailedSending() {

View File

@ -0,0 +1,38 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.space
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
class DefaultSpace(private val room: Room) : Space {
override fun asRoom(): Room {
return room
}
override suspend fun addRoom(roomId: String) {
asRoom().sendStateEvent(
eventType = EventType.STATE_SPACE_CHILD,
stateKey = roomId,
body = SpaceChildContent(present = true).toContent()
)
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.space
import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.api.session.space.SpaceService
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.RoomGetter
import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import javax.inject.Inject
internal class DefaultSpaceService @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
private val createRoomTask: CreateRoomTask,
private val joinRoomTask: JoinRoomTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
private val roomIdByAliasTask: GetRoomIdByAliasTask,
private val deleteRoomAliasTask: DeleteRoomAliasTask,
private val roomGetter: RoomGetter,
private val spaceSummaryDataSource: SpaceSummaryDataSource,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
private val taskExecutor: TaskExecutor
) : SpaceService {
override suspend fun createSpace(params: CreateSpaceParams): String {
return createRoomTask.execute(params)
}
override fun getSpace(spaceId: String): Space? {
return roomGetter.getRoom(spaceId)
?.takeIf { it.roomSummary()?.roomType == RoomType.SPACE }
?.let { DefaultSpace(it) }
}
override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<SpaceSummary>> {
return spaceSummaryDataSource.getRoomSummariesLive(queryParams)
}
override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<SpaceSummary> {
return spaceSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams)
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.space
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmQuery
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.model.VersioningState
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.mapper.SpaceSummaryMapper
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.findByAlias
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.query.process
import org.matrix.android.sdk.internal.util.fetchCopyMap
import javax.inject.Inject
internal class SpaceSummaryDataSource @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
private val spaceSummaryMapper: SpaceSummaryMapper
) {
fun getSpaceSummary(roomIdOrAlias: String): SpaceSummary? {
return monarchy
.fetchCopyMap({
if (roomIdOrAlias.startsWith("!")) {
// It's a roomId
SpaceSummaryEntity.where(it, roomId = roomIdOrAlias).findFirst()
} else {
// Assume it's a room alias
SpaceSummaryEntity.findByAlias(it, roomIdOrAlias)
}
}, { entity, _ ->
spaceSummaryMapper.map(entity)
})
}
fun getSpaceSummaryLive(roomId: String): LiveData<Optional<SpaceSummary>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm -> SpaceSummaryEntity.where(realm, roomId).isNotEmpty(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.DISPLAY_NAME) },
{ spaceSummaryMapper.map(it) }
)
return Transformations.map(liveData) { results ->
results.firstOrNull().toOptional()
}
}
fun getSpaceSummaries(queryParams: SpaceSummaryQueryParams): List<SpaceSummary> {
return monarchy.fetchAllMappedSync(
{ spaceSummariesQuery(it, queryParams) },
{ spaceSummaryMapper.map(it) }
)
}
fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<SpaceSummary>> {
return monarchy.findAllMappedWithChanges(
{ spaceSummariesQuery(it, queryParams) },
{ spaceSummaryMapper.map(it) }
)
}
private fun spaceSummariesQuery(realm: Realm, queryParams: SpaceSummaryQueryParams): RealmQuery<SpaceSummaryEntity> {
val query = SpaceSummaryEntity.where(realm)
query.process(SpaceSummaryEntityFields.SPACE_ID, queryParams.roomId)
query.process(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.DISPLAY_NAME, queryParams.displayName)
query.process(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.CANONICAL_ALIAS, queryParams.canonicalAlias)
query.process(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.MEMBERSHIP_STR, queryParams.memberships)
query.notEqualTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
return query
}
}

View File

@ -52,6 +52,7 @@ import im.vector.app.features.devtools.RoomDevToolStateEventListFragment
import im.vector.app.features.discovery.DiscoverySettingsFragment
import im.vector.app.features.discovery.change.SetIdentityServerFragment
import im.vector.app.features.grouplist.GroupListFragment
import im.vector.app.features.grouplist.SpaceListFragment
import im.vector.app.features.home.HomeDetailFragment
import im.vector.app.features.home.HomeDrawerFragment
import im.vector.app.features.home.LoadingFragment
@ -145,6 +146,11 @@ interface FragmentModule {
@FragmentKey(GroupListFragment::class)
fun bindGroupListFragment(fragment: GroupListFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SpaceListFragment::class)
fun bindSpaceListFragment(fragment: SpaceListFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomDetailFragment::class)

View File

@ -35,6 +35,7 @@ import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
import im.vector.app.features.grouplist.SelectedGroupDataSource
import im.vector.app.features.grouplist.SelectedSpaceDataSource
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
@ -115,6 +116,8 @@ interface VectorComponent {
fun selectedGroupStore(): SelectedGroupDataSource
fun selectedSpaceStore(): SelectedSpaceDataSource
fun roomDetailPendingActionStore(): RoomDetailPendingActionStore
fun activeSessionObservableStore(): ActiveSessionDataSource

View File

@ -46,7 +46,8 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
PLAIN("/plain", "<message>", R.string.command_description_plain),
DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session),
CONFETTI("/confetti", "<message>", R.string.command_confetti),
SNOW("/snow", "<message>", R.string.command_snow);
SNOW("/snow", "<message>", R.string.command_snow),
CREATE_SPACE("/createspace", "<name> <invitee>*", R.string.command_description_create_space);
val length
get() = command.length + 1

View File

@ -300,6 +300,18 @@ object CommandParser {
val message = textMessage.substring(Command.SNOW.command.length).trim()
ParsedCommand.SendChatEffect(ChatEffect.SNOW, message)
}
Command.CREATE_SPACE.command -> {
val rawCommand = textMessage.substring(Command.CREATE_SPACE.command.length).trim()
val split = rawCommand.split(" ").map { it.trim() }
if (split.isEmpty()) {
ParsedCommand.ErrorSyntax(Command.CREATE_SPACE)
} else {
ParsedCommand.CreateSpace(
split[0],
split.subList(1, split.size)
)
}
}
else -> {
// Unknown command
ParsedCommand.ErrorUnknownSlashCommand(slashCommand)

View File

@ -57,4 +57,5 @@ sealed class ParsedCommand {
class SendPoll(val question: String, val options: List<String>) : ParsedCommand()
object DiscardSession : ParsedCommand()
class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand()
class CreateSpace(val name: String, val invitees: List<String>) : ParsedCommand()
}

View File

@ -30,6 +30,7 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import io.reactivex.Observable
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.group.groupSummaryQueryParams
@ -96,8 +97,10 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
private fun handleSelectGroup(action: GroupListAction.SelectGroup) = withState { state ->
if (state.selectedGroup?.groupId != action.groupSummary.groupId) {
// We take care of refreshing group data when selecting to be sure we get all the rooms and users
viewModelScope.launch {
session.getGroup(action.groupSummary.groupId)?.fetchGroupData()
tryOrNull {
viewModelScope.launch {
session.getGroup(action.groupSummary.groupId)?.fetchGroupData()
}
}
setState { copy(selectedGroup = action.groupSummary) }
}

View File

@ -0,0 +1,62 @@
/*
* 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.features.grouplist
import android.content.res.Resources
import android.util.TypedValue
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
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
@EpoxyModelClass(layout = R.layout.item_space)
abstract class HomeSpaceSummaryItem : VectorEpoxyModel<HomeSpaceSummaryItem.Holder>() {
@EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.setOnClickListener { listener?.invoke() }
holder.groupNameView.text = holder.view.context.getString(R.string.group_details_home)
holder.rootView.isChecked = selected
holder.rootView.context.resources
holder.avatarImageView.background = ContextCompat.getDrawable(holder.view.context, R.drawable.space_home_background)
holder.avatarImageView.setImageResource(R.drawable.ic_space_home)
holder.avatarImageView.scaleType = ImageView.ScaleType.CENTER_INSIDE
}
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)
}
fun dpToPx(resources: Resources, dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.toFloat(),
resources.displayMetrics
).toInt()
}
}

View File

@ -0,0 +1,26 @@
/*
* 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.features.grouplist
import arrow.core.Option
import im.vector.app.core.utils.BehaviorDataSource
import org.matrix.android.sdk.api.session.space.SpaceSummary
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SelectedSpaceDataSource @Inject constructor() : BehaviorDataSource<Option<SpaceSummary>>(Option.empty())

View File

@ -0,0 +1,84 @@
/*
* 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.features.grouplist
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.StateView
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentGroupListBinding
import im.vector.app.features.home.HomeActivitySharedAction
import im.vector.app.features.home.HomeSharedActionViewModel
import im.vector.app.features.spaces.SpaceListAction
import im.vector.app.features.spaces.SpaceListViewEvents
import im.vector.app.features.spaces.SpacesListViewModel
import org.matrix.android.sdk.api.session.space.SpaceSummary
import javax.inject.Inject
class SpaceListFragment @Inject constructor(
val spaceListViewModelFactory: SpacesListViewModel.Factory,
private val spaceController: SpaceSummaryController
) : VectorBaseFragment<FragmentGroupListBinding>(), SpaceSummaryController.Callback {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private val viewModel: SpacesListViewModel by fragmentViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGroupListBinding {
return FragmentGroupListBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
spaceController.callback = this
views.stateView.contentView = views.groupListView
views.groupListView.configureWith(spaceController)
viewModel.observeViewEvents {
when (it) {
is SpaceListViewEvents.OpenSpaceSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup)
}.exhaustive
}
}
override fun onDestroyView() {
spaceController.callback = null
views.groupListView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { state ->
when (state.asyncSpaces) {
is Incomplete -> views.stateView.state = StateView.State.Loading
is Success -> views.stateView.state = StateView.State.Content
}
spaceController.update(state)
}
override fun onSpaceSelected(spaceSummary: SpaceSummary) {
viewModel.handle(SpaceListAction.SelectSpace(spaceSummary))
}
}

View File

@ -0,0 +1,76 @@
/*
* 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.features.grouplist
import com.airbnb.epoxy.EpoxyController
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.spaces.SpaceListViewState
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class SpaceSummaryController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider) : EpoxyController() {
var callback: Callback? = null
private var viewState: SpaceListViewState? = null
init {
requestModelBuild()
}
fun update(viewState: SpaceListViewState) {
this.viewState = viewState
requestModelBuild()
}
override fun buildModels() {
val nonNullViewState = viewState ?: return
buildGroupModels(nonNullViewState.asyncSpaces(), nonNullViewState.selectedSpace)
}
private fun buildGroupModels(summaries: List<SpaceSummary>?, selected: SpaceSummary?) {
if (summaries.isNullOrEmpty()) {
return
}
summaries.forEach { groupSummary ->
val isSelected = groupSummary.spaceId == selected?.spaceId
if (groupSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) {
homeSpaceSummaryItem {
id(groupSummary.spaceId)
selected(isSelected)
listener { callback?.onSpaceSelected(groupSummary) }
}
} else {
spaceSummaryItem {
avatarRenderer(avatarRenderer)
id(groupSummary.spaceId)
matrixItem(groupSummary.toMatrixItem())
selected(isSelected)
listener { callback?.onSpaceSelected(groupSummary) }
}
}
}
}
interface Callback {
fun onSpaceSelected(spaceSummary: SpaceSummary)
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.features.grouplist
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.platform.CheckableConstraintLayout
import im.vector.app.features.home.AvatarRenderer
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 lateinit var matrixItem: MatrixItem
@EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.setOnClickListener { listener?.invoke() }
holder.groupNameView.text = matrixItem.displayName
holder.rootView.isChecked = selected
avatarRenderer.renderSpace(matrixItem, holder.avatarImageView)
}
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)
}
}

View File

@ -35,6 +35,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.glide.GlideRequest
import im.vector.app.core.glide.GlideRequests
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import jp.wasabeef.glide.transformations.BlurTransformation
import jp.wasabeef.glide.transformations.ColorFilterTransformation
@ -48,7 +49,8 @@ import javax.inject.Inject
*/
class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val matrixItemColorProvider: MatrixItemColorProvider) {
private val matrixItemColorProvider: MatrixItemColorProvider,
private val dimensionConverter: DimensionConverter) {
companion object {
private const val THUMBNAIL_SIZE = 250
@ -61,6 +63,23 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
DrawableImageViewTarget(imageView))
}
@UiThread
fun renderSpace(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) {
val placeholder = getSpacePlaceholderDrawable(matrixItem)
val resolvedUrl = resolvedUrl(matrixItem.avatarUrl)
glideRequests
.load(resolvedUrl)
.transform(MultiTransformation(CenterCrop(), RoundedCorners(dimensionConverter.dpToPx(8))))
.placeholder(placeholder)
.into(DrawableImageViewTarget(imageView))
}
fun renderSpace(matrixItem: MatrixItem, imageView: ImageView) {
renderSpace(
matrixItem,
imageView, GlideApp.with(imageView))
}
fun clear(imageView: ImageView) {
// It can be called after recycler view is destroyed, just silently catch
tryOrNull { GlideApp.with(imageView).clear(imageView) }
@ -159,6 +178,16 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
.buildRound(matrixItem.firstLetterOfDisplayName(), avatarColor)
}
@AnyThread
fun getSpacePlaceholderDrawable(matrixItem: MatrixItem): Drawable {
val avatarColor = matrixItemColorProvider.getColor(matrixItem)
return TextDrawable.builder()
.beginConfig()
.bold()
.endConfig()
.buildRoundRect(matrixItem.firstLetterOfDisplayName(), avatarColor, dimensionConverter.dpToPx(8))
}
// PRIVATE API *********************************************************************************
private fun buildGlideRequest(glideRequests: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> {

View File

@ -22,7 +22,10 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -47,11 +50,13 @@ import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.popup.VerificationVectorAlert
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
import im.vector.app.features.spaces.ALL_COMMUNITIES_GROUP_ID
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.workers.signout.BannerState
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import im.vector.app.features.workers.signout.ServerBackupStatusViewState
import org.matrix.android.sdk.api.session.group.model.GroupSummary
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import timber.log.Timber
@ -130,6 +135,11 @@ class HomeDetailFragment @Inject constructor(
viewModel.selectSubscribe(this, HomeDetailViewState::groupSummary) { groupSummary ->
onGroupChange(groupSummary.orNull())
}
viewModel.selectSubscribe(this, HomeDetailViewState::spaceSummary) { spaceSummary ->
onSpaceChange(spaceSummary.orNull())
}
viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode ->
switchDisplayMode(displayMode)
}
@ -243,6 +253,24 @@ class HomeDetailFragment @Inject constructor(
}
}
private fun onSpaceChange(spaceSummary: SpaceSummary?) {
spaceSummary?.let {
// Use GlideApp with activity context to avoid the glideRequests to be paused
if (spaceSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) {
// Special case
views.groupToolbarAvatarImageView.background = ContextCompat.getDrawable(requireContext(), R.drawable.space_home_background)
views.groupToolbarAvatarImageView.scaleType = ImageView.ScaleType.CENTER_INSIDE
views.groupToolbarAvatarImageView.setImageResource(R.drawable.ic_space_home)
views.groupToolbarSpaceTitleView.isVisible = false
} else {
views.groupToolbarAvatarImageView.background = null
avatarRenderer.renderSpace(it.toMatrixItem(), views.groupToolbarAvatarImageView, GlideApp.with(requireActivity()))
views.groupToolbarSpaceTitleView.isVisible = true
views.groupToolbarSpaceTitleView.text = spaceSummary.displayName
}
}
}
private fun setupKeysBackupBanner() {
serverBackupStatusViewModel
.subscribe(this) {

View File

@ -28,6 +28,7 @@ import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.grouplist.SelectedGroupDataSource
import im.vector.app.features.grouplist.SelectedSpaceDataSource
import im.vector.app.features.ui.UiStateRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -75,6 +76,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
init {
observeSyncState()
observeSelectedGroupStore()
observeSelectedSpaceStore()
observeRoomSummaries()
}
@ -137,6 +139,17 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
.disposeOnClear()
}
private fun observeSelectedSpaceStore() {
selectedSpaceStore
.observe()
.subscribe {
setState {
copy(spaceSummary = it)
}
}
.disposeOnClear()
}
private fun observeRoomSummaries() {
session.getPagedRoomSummariesLive(
roomSummaryQueryParams {

View File

@ -22,10 +22,12 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.group.model.GroupSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.sync.SyncState
data class HomeDetailViewState(
val groupSummary: Option<GroupSummary> = Option.empty(),
val spaceSummary: Option<SpaceSummary> = Option.empty(),
val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE,
val notificationCountCatchup: Int = 0,

View File

@ -31,6 +31,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.databinding.FragmentHomeDrawerBinding
import im.vector.app.features.grouplist.GroupListFragment
import im.vector.app.features.grouplist.SpaceListFragment
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.usercode.UserCodeActivity
@ -58,7 +59,11 @@ class HomeDrawerFragment @Inject constructor(
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
if (savedInstanceState == null) {
replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java)
if (vectorPreferences.labSpaces()) {
replaceChildFragment(R.id.homeDrawerGroupListContainer, SpaceListFragment::class.java)
} else {
replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java)
}
}
session.getUserLive(session.myUserId).observeK(viewLifecycleOwner) { optionalUser ->
val user = optionalUser?.getOrNull()

View File

@ -97,6 +97,8 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import org.matrix.android.sdk.api.util.appendParamToUrl
import org.matrix.android.sdk.api.util.toOptional
@ -821,6 +823,22 @@ class RoomDetailViewModel @AssistedInject constructor(
)
}
}
is ParsedCommand.CreateSpace -> {
viewModelScope.launch(Dispatchers.IO) {
try {
val params = CreateSpaceParams().apply {
name = slashCommandResult.name
invitedUserIds.addAll(slashCommandResult.invitees)
}
val spaceId = session.spaceService().createSpace(params)
session.spaceService().getSpace(spaceId)?.addRoom(state.roomId)
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
}
}
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
}.exhaustive
}
is SendMode.EDIT -> {

View File

@ -89,19 +89,19 @@ class CreateRoomController @Inject constructor(
enabled(enableFormElement)
title(stringProvider.getString(R.string.create_room_public_title))
summary(stringProvider.getString(R.string.create_room_public_description))
switchChecked(viewState.roomType is CreateRoomViewState.RoomType.Public)
showDivider(viewState.roomType !is CreateRoomViewState.RoomType.Public)
switchChecked(viewState.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public)
showDivider(viewState.roomVisibilityType !is CreateRoomViewState.RoomVisibilityType.Public)
listener { value ->
listener?.setIsPublic(value)
}
}
if (viewState.roomType is CreateRoomViewState.RoomType.Public) {
if (viewState.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public) {
// Room alias for public room
roomAliasEditItem {
id("alias")
enabled(enableFormElement)
value(viewState.roomType.aliasLocalPart)
value(viewState.roomVisibilityType.aliasLocalPart)
homeServer(":" + viewState.homeServerName)
errorMessage(
roomAliasErrorFormatter.format(

View File

@ -76,7 +76,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
setState {
copy(
isEncrypted = roomType is CreateRoomViewState.RoomType.Private && adminE2EByDefault,
isEncrypted = roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Private && adminE2EByDefault,
hsAdminHasDisabledE2E = !adminE2EByDefault
)
}
@ -147,14 +147,14 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
private fun setIsPublic(action: CreateRoomAction.SetIsPublic) = setState {
if (action.isPublic) {
copy(
roomType = CreateRoomViewState.RoomType.Public(""),
roomVisibilityType = CreateRoomViewState.RoomVisibilityType.Public(""),
// Reset any error in the form about alias
asyncCreateRoomRequest = Uninitialized,
isEncrypted = false
)
} else {
copy(
roomType = CreateRoomViewState.RoomType.Private,
roomVisibilityType = CreateRoomViewState.RoomVisibilityType.Private,
isEncrypted = adminE2EByDefault
)
}
@ -162,10 +162,10 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
private fun setRoomAliasLocalPart(action: CreateRoomAction.SetRoomAliasLocalPart) {
withState { state ->
if (state.roomType is CreateRoomViewState.RoomType.Public) {
if (state.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public) {
setState {
copy(
roomType = CreateRoomViewState.RoomType.Public(action.aliasLocalPart),
roomVisibilityType = CreateRoomViewState.RoomVisibilityType.Public(action.aliasLocalPart),
// Reset any error in the form about alias
asyncCreateRoomRequest = Uninitialized
)
@ -191,15 +191,15 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
name = state.roomName.takeIf { it.isNotBlank() }
topic = state.roomTopic.takeIf { it.isNotBlank() }
avatarUri = state.avatarUri
when (state.roomType) {
is CreateRoomViewState.RoomType.Public -> {
when (state.roomVisibilityType) {
is CreateRoomViewState.RoomVisibilityType.Public -> {
// Directory visibility
visibility = RoomDirectoryVisibility.PUBLIC
// Preset
preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
roomAliasName = state.roomType.aliasLocalPart
roomAliasName = state.roomVisibilityType.aliasLocalPart
}
is CreateRoomViewState.RoomType.Private -> {
is CreateRoomViewState.RoomVisibilityType.Private -> {
// Directory visibility
visibility = RoomDirectoryVisibility.PRIVATE
// Preset

View File

@ -26,7 +26,7 @@ data class CreateRoomViewState(
val avatarUri: Uri? = null,
val roomName: String = "",
val roomTopic: String = "",
val roomType: RoomType = RoomType.Private,
val roomVisibilityType: RoomVisibilityType = RoomVisibilityType.Private,
val isEncrypted: Boolean = false,
val showAdvanced: Boolean = false,
val disableFederation: Boolean = false,
@ -45,10 +45,10 @@ data class CreateRoomViewState(
fun isEmpty() = avatarUri == null
&& roomName.isEmpty()
&& roomTopic.isEmpty()
&& (roomType as? RoomType.Public)?.aliasLocalPart?.isEmpty().orTrue()
&& (roomVisibilityType as? RoomVisibilityType.Public)?.aliasLocalPart?.isEmpty().orTrue()
sealed class RoomType {
object Private : RoomType()
data class Public(val aliasLocalPart: String) : RoomType()
sealed class RoomVisibilityType {
object Private : RoomVisibilityType()
data class Public(val aliasLocalPart: String) : RoomVisibilityType()
}
}

View File

@ -156,6 +156,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY"
const val SETTINGS_LABS_USE_SPACES = "SETTINGS_LABS_USE_SPACES"
// SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS
private const val SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM = "SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM"
const val SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB = "SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB"
@ -306,6 +308,10 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB, false)
}
fun labSpaces(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_USE_SPACES, false)
}
fun failFast(): Boolean {
return BuildConfig.DEBUG || (developerMode() && defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY, false))
}

View File

@ -0,0 +1,172 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces
import arrow.core.Option
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.grouplist.SelectedSpaceDataSource
import im.vector.app.features.grouplist.SpaceListFragment
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
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.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.rx.rx
const val ALL_COMMUNITIES_GROUP_ID = "+ALL_COMMUNITIES_GROUP_ID"
sealed class SpaceListAction : VectorViewModelAction {
data class SelectSpace(val spaceSummary: SpaceSummary) : SpaceListAction()
}
/**
* Transient events for group list screen
*/
sealed class SpaceListViewEvents : VectorViewEvents {
object OpenSpaceSummary : SpaceListViewEvents()
}
data class SpaceListViewState(
val asyncSpaces: Async<List<SpaceSummary>> = Uninitialized,
val selectedSpace: SpaceSummary? = null
) : MvRxState
class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: SpaceListViewState,
private val selectedSpaceDataSource: SelectedSpaceDataSource,
private val session: Session,
private val stringProvider: StringProvider
) : VectorViewModel<SpaceListViewState, SpaceListAction, SpaceListViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: SpaceListViewState): SpacesListViewModel
}
companion object : MvRxViewModelFactory<SpacesListViewModel, SpaceListViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: SpaceListViewState): SpacesListViewModel {
val groupListFragment: SpaceListFragment = (viewModelContext as FragmentViewModelContext).fragment()
return groupListFragment.spaceListViewModelFactory.create(state)
}
}
private var currentGroupId = ""
init {
observeGroupSummaries()
observeSelectionState()
}
private fun observeSelectionState() {
selectSubscribe(SpaceListViewState::selectedSpace) { spaceSummary ->
if (spaceSummary != null) {
// We only want to open group if the updated selectedGroup is a different one.
if (currentGroupId != spaceSummary.spaceId) {
currentGroupId = spaceSummary.spaceId
_viewEvents.post(SpaceListViewEvents.OpenSpaceSummary)
}
val optionGroup = Option.just(spaceSummary)
selectedSpaceDataSource.post(optionGroup)
} else {
// If selected group is null we force to default. It can happens when leaving the selected group.
setState {
copy(selectedSpace = this.asyncSpaces()?.find { it.spaceId == ALL_COMMUNITIES_GROUP_ID })
}
}
}
}
override fun handle(action: SpaceListAction) {
when (action) {
is SpaceListAction.SelectSpace -> handleSelectSpace(action)
}
}
// PRIVATE METHODS *****************************************************************************
private fun handleSelectSpace(action: SpaceListAction.SelectSpace) = withState { state ->
if (state.selectedSpace?.spaceId != action.spaceSummary.spaceId) {
// We take care of refreshing group data when selecting to be sure we get all the rooms and users
// tryOrNull {
// viewModelScope.launch {
// session.getGroup(action.spaceSummary.groupId)?.fetchGroupData()
// }
// }
setState { copy(selectedSpace = action.spaceSummary) }
}
}
private fun observeGroupSummaries() {
val roomSummaryQueryParams = roomSummaryQueryParams() {
memberships = listOf(Membership.JOIN)
displayName = QueryStringValue.IsNotEmpty
excludeType = listOf(RoomType.MESSAGING, null)
}
Observable.combineLatest<SpaceSummary, List<SpaceSummary>, List<SpaceSummary>>(
session
.rx()
.liveUser(session.myUserId)
.map { optionalUser ->
SpaceSummary(
spaceId = ALL_COMMUNITIES_GROUP_ID,
roomSummary = RoomSummary(
roomId = ALL_COMMUNITIES_GROUP_ID,
membership = Membership.JOIN,
displayName = stringProvider.getString(R.string.group_all_communities),
avatarUrl = optionalUser.getOrNull()?.avatarUrl ?: "",
encryptionEventTs = 0,
isEncrypted = false,
typingUsers = emptyList()
),
children = emptyList()
)
},
session
.rx()
.liveSpaceSummaries(roomSummaryQueryParams),
BiFunction { allCommunityGroup, communityGroups ->
listOf(allCommunityGroup) + communityGroups
}
)
.execute { async ->
val currentSelectedGroupId = selectedSpace?.spaceId
val newSelectedGroup = if (currentSelectedGroupId != null) {
async()?.find { it.spaceId == currentSelectedGroupId }
} else {
async()?.firstOrNull()
}
copy(asyncSpaces = async, selectedSpace = newSelectedGroup)
}
}
}

View File

@ -3,7 +3,7 @@
<item android:state_checked="true">
<shape>
<solid android:color="@android:color/transparent" />
<solid android:color="?colorAccent" />
<corners android:radius="4dp" />
</shape>
</item>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<layer-list>
<item>
<shape>
<solid android:color="@android:color/transparent" />
</shape>
</item>
<item android:gravity="center_vertical|start" >
<shape>
<size android:width="4dp" android:height="40dp" />
<solid android:color="?colorAccent" />
<corners android:bottomRightRadius="8dp" android:topRightRadius="8dp" />
</shape>
</item>
</layer-list>
</item>
<item>
<shape>
<solid android:color="@android:color/transparent" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="4dp"
android:height="32dp"
android:viewportWidth="4"
android:viewportHeight="32">
<path
android:pathData="M0,0C2.2091,0 4,1.7909 4,4V28C4,30.2091 2.2091,32 0,32V0Z"
android:fillColor="#0DBD8B"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="22dp"
android:viewportWidth="18"
android:viewportHeight="22">
<path
android:pathData="M12.75,21.62L12.75,15C12.75,14.4477 12.3023,14 11.75,14L6.25,14C5.6977,14 5.25,14.4477 5.25,15L5.25,21.6047C4.1731,21.6088 3.1268,21.6166 2.1586,21.6292C1.0345,21.6438 0.111,20.7402 0.111,19.616L0.111,8.5055C0.111,8.1143 0.2828,7.7429 0.5808,7.4896L8.0284,1.1591C8.5886,0.683 9.4112,0.683 9.9714,1.1591L17.419,7.4896C17.717,7.7429 17.8888,8.1119 17.8888,8.5031L17.8888,19.6452C17.8888,20.7595 16.981,21.6557 15.8667,21.6451C15.0172,21.637 13.9473,21.6278 12.75,21.62Z"
android:strokeWidth="1"
android:fillColor="#3F3D3D"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="40dp" android:height="40dp"/>
<solid android:color="?riotx_background" />
<stroke android:width="1dp" android:color="@color/riotx_disabled_accent"/>
<corners android:radius="8dp" />
</shape>

View File

@ -22,24 +22,49 @@
android:orientation="horizontal">
<ImageView
android:layout_gravity="center_vertical"
android:id="@+id/groupToolbarAvatarImageView"
android:layout_width="32dp"
android:layout_height="32dp"
android:contentDescription="@string/a11y_open_drawer"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/groupToolbarTitleView"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
tools:text="@tools:sample/lorem/random" />
android:layout_weight="1"
android:gravity="start"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<TextView
android:id="@+id/groupToolbarTitleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="start"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/groupToolbarSpaceTitleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:visibility="visible"
android:visibility="gone"
android:ellipsize="end"
android:gravity="start"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
</LinearLayout>
@ -60,10 +85,10 @@
android:background="?riotx_keys_backup_banner_accent_color"
android:minHeight="67dp"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/syncStateView" />
app:layout_constraintTop_toBottomOf="@id/syncStateView"
tools:visibility="visible" />
<im.vector.app.core.ui.views.CurrentCallsView
android:id="@+id/activeCallView"

View File

@ -0,0 +1,63 @@
<?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="65dp"
android:background="@drawable/bg_space_item"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<ImageView
android:id="@+id/groupAvatarImageView"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:duplicateParentState="true"
app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<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_toTopOf="@+id/groupBottomSeparator"
app:layout_constraintEnd_toStartOf="@+id/groupAvatarChevron"
app:layout_constraintStart_toEndOf="@+id/groupAvatarImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem/random" />
<ImageView
android:id="@+id/groupAvatarChevron"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="21dp"
android:src="@drawable/ic_arrow_right"
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

@ -2183,6 +2183,8 @@
<string name="labs_swipe_to_reply_in_timeline">Enable swipe to reply in timeline</string>
<string name="labs_show_unread_notifications_as_tab">Add a dedicated tab for unread notifications on main screen.</string>
<string name="labs_experimental_spaces">Enable Spaces (formerly known as groups as rooms) to allow users to organise rooms into more useful groups.</string>
<string name="link_copied_to_clipboard">Link copied to clipboard</string>
<string name="add_by_matrix_id">Add by matrix ID</string>
@ -3242,6 +3244,8 @@
<string name="dev_tools_success_state_event">State event sent!</string>
<string name="dev_tools_event_content_hint">Event content</string>
<string name="command_description_create_space">Create a community</string>
<string name="event_status_a11y_sending">Sending</string>
<string name="event_status_a11y_sent">Sent</string>
<string name="event_status_a11y_failed">Failed</string>

View File

@ -45,4 +45,10 @@
android:title="@string/labs_show_unread_notifications_as_tab" />
<!--</im.vector.app.core.preference.VectorPreferenceCategory>-->
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_LABS_USE_SPACES"
android:title="@string/labs_experimental_spaces" />
</androidx.preference.PreferenceScreen>