From c5fa0a413f5ae3eec898e7c4fdb8bac0454868df Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 29 Dec 2020 17:34:25 +0100 Subject: [PATCH] Space first commit --- .../org/matrix/android/sdk/rx/RxSession.kt | 9 + .../matrix/android/sdk/api/session/Session.kt | 6 + .../sdk/api/session/events/model/EventType.kt | 1 + .../session/room/RoomSummaryQueryParams.kt | 4 + .../api/session/room/model/IRoomSummary.kt | 31 ++++ .../sdk/api/session/room/model/RoomSummary.kt | 27 +-- .../sdk/api/session/room/model/RoomType.kt | 23 +++ .../room/model/create/CreateRoomParams.kt | 15 +- .../room/model/create/RoomCreateContent.kt | 4 +- .../api/session/space/CreateSpaceParams.kt | 27 +++ .../android/sdk/api/session/space/Space.kt | 26 +++ .../sdk/api/session/space/SpaceService.kt | 45 +++++ .../sdk/api/session/space/SpaceSummary.kt | 26 +++ .../session/space/model/SpaceChildContent.kt | 52 ++++++ .../matrix/android/sdk/api/util/MatrixItem.kt | 9 +- .../database/RealmSessionStoreMigration.kt | 20 +- .../database/mapper/RoomSummaryMapper.kt | 3 +- .../database/mapper/SpaceSummaryMapper.kt | 34 ++++ .../database/model/SessionRealmModule.kt | 3 +- .../database/model/SpaceSummaryEntity.kt | 41 +++++ .../query/SpaceSummaryEntityQueries.kt | 55 ++++++ .../sdk/internal/session/DefaultSession.kt | 6 +- .../sdk/internal/session/room/RoomModule.kt | 5 + .../relationship/RoomRelationshipHelper.kt | 50 +++++ .../room/summary/RoomSummaryDataSource.kt | 4 + .../room/summary/RoomSummaryUpdater.kt | 18 ++ .../internal/session/space/DefaultSpace.kt | 38 ++++ .../session/space/DefaultSpaceService.kt | 70 +++++++ .../session/space/SpaceSummaryDataSource.kt | 93 ++++++++++ .../im/vector/app/core/di/FragmentModule.kt | 6 + .../im/vector/app/core/di/VectorComponent.kt | 3 + .../im/vector/app/features/command/Command.kt | 3 +- .../app/features/command/CommandParser.kt | 12 ++ .../app/features/command/ParsedCommand.kt | 1 + .../features/grouplist/GroupListViewModel.kt | 7 +- .../grouplist/HomeSpaceSummaryItem.kt | 62 +++++++ .../grouplist/SelectedSpaceDataSource.kt | 26 +++ .../features/grouplist/SpaceListFragment.kt | 84 +++++++++ .../grouplist/SpaceSummaryController.kt | 76 ++++++++ .../features/grouplist/SpaceSummaryItem.kt | 57 ++++++ .../app/features/home/AvatarRenderer.kt | 31 +++- .../app/features/home/HomeDetailFragment.kt | 28 +++ .../app/features/home/HomeDetailViewModel.kt | 13 ++ .../app/features/home/HomeDetailViewState.kt | 2 + .../app/features/home/HomeDrawerFragment.kt | 7 +- .../home/room/detail/RoomDetailViewModel.kt | 18 ++ .../createroom/CreateRoomController.kt | 8 +- .../createroom/CreateRoomViewModel.kt | 18 +- .../createroom/CreateRoomViewState.kt | 10 +- .../features/settings/VectorPreferences.kt | 6 + .../features/spaces/SpacesListViewModel.kt | 172 ++++++++++++++++++ .../src/main/res/drawable/bg_group_item.xml | 2 +- .../src/main/res/drawable/bg_space_item.xml | 27 +++ .../res/drawable/ic_selected_community.xml | 9 + .../src/main/res/drawable/ic_space_home.xml | 12 ++ .../res/drawable/space_home_background.xml | 13 ++ .../main/res/layout/fragment_home_detail.xml | 51 ++++-- vector/src/main/res/layout/item_space.xml | 63 +++++++ vector/src/main/res/values/strings.xml | 4 + .../src/main/res/xml/vector_settings_labs.xml | 6 + 60 files changed, 1523 insertions(+), 59 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/IRoomSummary.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomType.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/CreateSpaceParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceSummary.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/SpaceSummaryMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/SpaceSummaryEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomRelationshipHelper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryDataSource.kt create mode 100644 vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/grouplist/SelectedSpaceDataSource.kt create mode 100644 vector/src/main/java/im/vector/app/features/grouplist/SpaceListFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryController.kt create mode 100644 vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt create mode 100644 vector/src/main/res/drawable/bg_space_item.xml create mode 100644 vector/src/main/res/drawable/ic_selected_community.xml create mode 100644 vector/src/main/res/drawable/ic_space_home.xml create mode 100644 vector/src/main/res/drawable/space_home_background.xml create mode 100644 vector/src/main/res/layout/item_space.xml diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt index 0fe2b01576..7cc0d69bb9 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt @@ -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> { + return session.spaceService().getSpaceSummariesLive(queryParams).asObservable() + .startWithCallable { + session.spaceService().getSpaceSummaries(queryParams) + } + } + fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable> { return session.getBreadcrumbsLive(queryParams).asObservable() .startWithCallable { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index a15799d862..5f442c33f9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -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. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 905e18b8e8..9d8f18e912 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt index 7e04ebb5f2..c8d52302e9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt @@ -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, val roomCategoryFilter: RoomCategoryFilter?, val roomTagQueryFilter: RoomTagQueryFilter? + val excludeType: List ) { class Builder { @@ -46,6 +48,7 @@ data class RoomSummaryQueryParams( var memberships: List = Membership.all() var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL var roomTagQueryFilter: RoomTagQueryFilter? = null + var excludeType: List = listOf(RoomType.SPACE) fun build() = RoomSummaryQueryParams( roomId = roomId, @@ -54,6 +57,7 @@ data class RoomSummaryQueryParams( memberships = memberships, roomCategoryFilter = roomCategoryFilter, roomTagQueryFilter = roomTagQueryFilter + excludeType = excludeType ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/IRoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/IRoomSummary.kt new file mode 100644 index 0000000000..1724f00c99 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/IRoomSummary.kt @@ -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 + val joinedMembersCount: Int? + val invitedMembersCount: Int? + val otherMemberIds: List + val roomType: String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt index 9455a83aff..ac87a16911 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt @@ -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 = 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 = emptyList(), + override val joinedMembersCount: Int? = 0, + override val invitedMembersCount: Int? = 0, val latestPreviewableEvent: TimelineEvent? = null, - val otherMemberIds: List = emptyList(), + override val otherMemberIds: List = 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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomType.kt new file mode 100644 index 0000000000..3958d45d0b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomType.kt @@ -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" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt index 80e3741a0c..6009649314 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt @@ -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" } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt index 0b595b1b2b..52e5c0e9c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt @@ -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 ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/CreateSpaceParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/CreateSpaceParams.kt new file mode 100644 index 0000000000..0caa7af14c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/CreateSpaceParams.kt @@ -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 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt new file mode 100644 index 0000000000..75480282fa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt @@ -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) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt new file mode 100644 index 0000000000..0c3461f1ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -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> + + fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceSummary.kt new file mode 100644 index 0000000000..d2be2f18f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceSummary.kt @@ -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 by roomSummary diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt new file mode 100644 index 0000000000..f65318b543 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt @@ -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? = 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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt index db229a6453..a792248764 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt @@ -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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 1daae906f2..2c06a4e8f7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -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")!!) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index 6dc70b60fc..c74eb4460d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -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 ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/SpaceSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/SpaceSummaryMapper.kt new file mode 100644 index 0000000000..9dee99d7fe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/SpaceSummaryMapper.kt @@ -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) + } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index 6e6096cf8a..8c5bb8e990 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -61,6 +61,7 @@ import io.realm.annotations.RealmModule CurrentStateEventEntity::class, UserAccountDataEntity::class, ScalarTokenEntity::class, - WellknownIntegrationManagerConfigEntity::class + WellknownIntegrationManagerConfigEntity::class, + SpaceSummaryEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceSummaryEntity.kt new file mode 100644 index 0000000000..ca54655022 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceSummaryEntity.kt @@ -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 = 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 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/SpaceSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/SpaceSummaryEntityQueries.kt new file mode 100644 index 0000000000..b6403c596f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/SpaceSummaryEntityQueries.kt @@ -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 { + val query = realm.where() + 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() + .isNotNull(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`) + .equalTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.CANONICAL_ALIAS, roomAlias) + .findFirst() + if (spaceSummary != null) { + return spaceSummary + } + return realm.where() + .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(roomId).also { + it.roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 821a9cba8c..ecb680c691 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -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, private val callSignalingService: Lazy, @UnauthenticatedWithCertificate - private val unauthenticatedWithCertificateOkHttpClient: Lazy + private val unauthenticatedWithCertificateOkHttpClient: Lazy, + private val spaceService: Lazy ) : 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() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 5133f72932..8f3445bec3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomRelationshipHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomRelationshipHelper.kt new file mode 100644 index 0000000000..4025861caa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomRelationshipHelper.kt @@ -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 { + return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD) + .findAll() + .filter { ContentMapper.map(it.root?.content).toModel()?.present == true } + .mapNotNull { + // ContentMapper.map(it.root?.content).toModel() + it.roomId + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index dd3fbe04b2..576e7f4eba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -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 } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 7913bf71a2..f8a3495aa2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -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()?.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() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt new file mode 100644 index 0000000000..ae71ee5cf2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt @@ -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() + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt new file mode 100644 index 0000000000..3d9e7d7764 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -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> { + return spaceSummaryDataSource.getRoomSummariesLive(queryParams) + } + + override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List { + return spaceSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryDataSource.kt new file mode 100644 index 0000000000..c15e81c287 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryDataSource.kt @@ -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> { + 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 { + return monarchy.fetchAllMappedSync( + { spaceSummariesQuery(it, queryParams) }, + { spaceSummaryMapper.map(it) } + ) + } + + fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> { + return monarchy.findAllMappedWithChanges( + { spaceSummariesQuery(it, queryParams) }, + { spaceSummaryMapper.map(it) } + ) + } + + private fun spaceSummariesQuery(realm: Realm, queryParams: SpaceSummaryQueryParams): RealmQuery { + 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 + } +} diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 430aee5468..1a3719e03f 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index 4b88ff6767..3a197d3f83 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 66d88f149a..b29d061dfb 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -46,7 +46,8 @@ enum class Command(val command: String, val parameters: String, @StringRes val d PLAIN("/plain", "", R.string.command_description_plain), DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session), CONFETTI("/confetti", "", R.string.command_confetti), - SNOW("/snow", "", R.string.command_snow); + SNOW("/snow", "", R.string.command_snow), + CREATE_SPACE("/createspace", " *", R.string.command_description_create_space); val length get() = command.length + 1 diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index d458751364..57466ddf98 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index d17faeafb8..1017b29234 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -57,4 +57,5 @@ sealed class ParsedCommand { class SendPoll(val question: String, val options: List) : ParsedCommand() object DiscardSession : ParsedCommand() class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand() + class CreateSpace(val name: String, val invitees: List) : ParsedCommand() } diff --git a/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt b/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt index 4b187f83ca..0f6f77783d 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt new file mode 100644 index 0000000000..ade86a9d89 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt @@ -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() { + + @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(R.id.groupAvatarImageView) + val groupNameView by bind(R.id.groupNameView) + val rootView by bind(R.id.itemGroupLayout) + } + + fun dpToPx(resources: Resources, dp: Int): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp.toFloat(), + resources.displayMetrics + ).toInt() + } +} diff --git a/vector/src/main/java/im/vector/app/features/grouplist/SelectedSpaceDataSource.kt b/vector/src/main/java/im/vector/app/features/grouplist/SelectedSpaceDataSource.kt new file mode 100644 index 0000000000..d95251c271 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/grouplist/SelectedSpaceDataSource.kt @@ -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.empty()) diff --git a/vector/src/main/java/im/vector/app/features/grouplist/SpaceListFragment.kt b/vector/src/main/java/im/vector/app/features/grouplist/SpaceListFragment.kt new file mode 100644 index 0000000000..4c783cb2d4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/grouplist/SpaceListFragment.kt @@ -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(), 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)) + } +} diff --git a/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryController.kt b/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryController.kt new file mode 100644 index 0000000000..c7a27266fd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryController.kt @@ -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?, 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) + } +} diff --git a/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryItem.kt new file mode 100644 index 0000000000..1a710b764c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryItem.kt @@ -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() { + + @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(R.id.groupAvatarImageView) + val groupNameView by bind(R.id.groupNameView) + val rootView by bind(R.id.itemGroupLayout) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index 1d673a2a07..1765372548 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 5def43b60b..de24be1a7d 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -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) { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index d6a8b075f4..a07a329a57 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt index 533c9166f9..dd316dcece 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt @@ -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 = Option.empty(), + val spaceSummary: Option = Option.empty(), val asyncRooms: Async> = Uninitialized, val displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE, val notificationCountCatchup: Int = 0, diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt index 59eb45607e..92be20367a 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 006a2c9b5f..bdf38719e2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -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 -> { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt index 94b419797d..e08f383512 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt @@ -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( diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt index af63f23a8c..33dc6bc054 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt index 4609693c8f..6bc19dfa20 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt @@ -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() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 9b043cfc7c..222c8da6b7 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -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)) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt new file mode 100644 index 0000000000..5daedcc984 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt @@ -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> = 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(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: SpaceListViewState): SpacesListViewModel + } + + companion object : MvRxViewModelFactory { + + @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, List>( + 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) + } + } +} diff --git a/vector/src/main/res/drawable/bg_group_item.xml b/vector/src/main/res/drawable/bg_group_item.xml index 9e48ebc725..ea39f5a9d0 100644 --- a/vector/src/main/res/drawable/bg_group_item.xml +++ b/vector/src/main/res/drawable/bg_group_item.xml @@ -3,7 +3,7 @@ - + diff --git a/vector/src/main/res/drawable/bg_space_item.xml b/vector/src/main/res/drawable/bg_space_item.xml new file mode 100644 index 0000000000..1cb879a0ca --- /dev/null +++ b/vector/src/main/res/drawable/bg_space_item.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_selected_community.xml b/vector/src/main/res/drawable/ic_selected_community.xml new file mode 100644 index 0000000000..e95b54aab3 --- /dev/null +++ b/vector/src/main/res/drawable/ic_selected_community.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_space_home.xml b/vector/src/main/res/drawable/ic_space_home.xml new file mode 100644 index 0000000000..e5935156f4 --- /dev/null +++ b/vector/src/main/res/drawable/ic_space_home.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/drawable/space_home_background.xml b/vector/src/main/res/drawable/space_home_background.xml new file mode 100644 index 0000000000..ec51c30a20 --- /dev/null +++ b/vector/src/main/res/drawable/space_home_background.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_home_detail.xml b/vector/src/main/res/layout/fragment_home_detail.xml index d25375f3b9..ba2b630efe 100644 --- a/vector/src/main/res/layout/fragment_home_detail.xml +++ b/vector/src/main/res/layout/fragment_home_detail.xml @@ -22,24 +22,49 @@ android:orientation="horizontal"> - + android:layout_weight="1" + android:gravity="start" + android:orientation="vertical" + android:paddingStart="8dp" + android:paddingEnd="8dp"> + + + + + + + @@ -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" /> + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 634b91bf90..c9cb4729f7 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2183,6 +2183,8 @@ Enable swipe to reply in timeline Add a dedicated tab for unread notifications on main screen. + Enable Spaces (formerly known as ‘groups as rooms’) to allow users to organise rooms into more useful groups. + Link copied to clipboard Add by matrix ID @@ -3242,6 +3244,8 @@ State event sent! Event content + Create a community + Sending Sent Failed diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index fef5a2fe9d..ed1ea222a2 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -45,4 +45,10 @@ android:title="@string/labs_show_unread_notifications_as_tab" /> + + + \ No newline at end of file