From c5fa0a413f5ae3eec898e7c4fdb8bac0454868df Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 29 Dec 2020 17:34:25 +0100 Subject: [PATCH 01/97] 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 From df341d8ea38a1d8a9cab1432e92db999163fd608 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 7 Jan 2021 10:43:00 +0100 Subject: [PATCH 02/97] Basic peeking preview and join and filter --- .../sdk/api/session/events/model/EventType.kt | 1 + .../api/session/room/peeking/PeekResult.kt | 2 + .../android/sdk/api/session/space/Space.kt | 2 + .../sdk/api/session/space/SpaceService.kt | 27 +++ .../session/space/model/SpaceChildContent.kt | 1 + .../sdk/internal/session/room/RoomModule.kt | 5 + .../relationship/RoomRelationshipHelper.kt | 2 +- .../room/summary/RoomSummaryUpdater.kt | 2 +- .../internal/session/space/DefaultSpace.kt | 8 + .../session/space/DefaultSpaceService.kt | 30 ++++ .../session/space/peeking/PeekSpaceTask.kt | 141 +++++++++++++++ .../session/space/peeking/SpacePeekResult.kt | 55 ++++++ vector/src/main/AndroidManifest.xml | 3 +- .../im/vector/app/core/di/FragmentModule.kt | 6 + .../im/vector/app/core/di/ViewModelModule.kt | 6 + .../im/vector/app/features/command/Command.kt | 3 +- .../app/features/command/CommandParser.kt | 8 +- .../app/features/command/ParsedCommand.kt | 1 + .../features/grouplist/SpaceListFragment.kt | 3 +- .../grouplist/SpaceSummaryController.kt | 69 ++++++-- .../vector/app/features/home/HomeActivity.kt | 4 + .../features/home/HomeActivitySharedAction.kt | 1 + .../home/room/detail/RoomDetailViewModel.kt | 11 ++ .../features/spaces/SpacePreviewActivity.kt | 72 ++++++++ .../SpacePreviewSharedActionViewModel.kt | 30 ++++ .../features/spaces/SpacesListViewModel.kt | 26 ++- .../features/spaces/preview/RoomChildItem.kt | 79 +++++++++ .../spaces/preview/SpacePreviewController.kt | 117 +++++++++++++ .../spaces/preview/SpacePreviewFragment.kt | 165 ++++++++++++++++++ .../spaces/preview/SpacePreviewState.kt | 31 ++++ .../spaces/preview/SpacePreviewViewAction.kt | 25 +++ .../spaces/preview/SpacePreviewViewEvents.kt | 26 +++ .../spaces/preview/SpacePreviewViewModel.kt | 135 ++++++++++++++ .../features/spaces/preview/SpaceTabView.kt | 51 ++++++ .../spaces/preview/SpaceTopSummaryItem.kt | 46 +++++ .../features/spaces/preview/SubSpaceItem.kt | 68 ++++++++ .../res/layout/fragment_space_preview.xml | 127 ++++++++++++++ .../main/res/layout/item_space_roomchild.xml | 115 ++++++++++++ .../main/res/layout/item_space_subspace.xml | 71 ++++++++ vector/src/main/res/layout/item_space_tab.xml | 11 ++ .../res/layout/item_space_top_summary.xml | 46 +++++ vector/src/main/res/values/strings.xml | 3 + 42 files changed, 1604 insertions(+), 31 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/SpacePreviewSharedActionViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/preview/RoomChildItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTabView.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTopSummaryItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt create mode 100644 vector/src/main/res/layout/fragment_space_preview.xml create mode 100644 vector/src/main/res/layout/item_space_roomchild.xml create mode 100644 vector/src/main/res/layout/item_space_subspace.xml create mode 100644 vector/src/main/res/layout/item_space_tab.xml create mode 100644 vector/src/main/res/layout/item_space_top_summary.xml 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 9d8f18e912..4be8eea856 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 @@ -52,6 +52,7 @@ object EventType { 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" +// const val STATE_SPACE_CHILD = "org.matrix.msc1772.space" /** * Note that this Event has been deprecated, see diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt index db70dadef3..a27e88aced 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt @@ -34,4 +34,6 @@ sealed class PeekResult { ) : PeekResult() object UnknownAlias : PeekResult() + + fun isSuccess() = this is Success } 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 index 75480282fa..88ac00cc55 100644 --- 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 @@ -23,4 +23,6 @@ interface Space { fun asRoom() : Room suspend fun addRoom(roomId: String) + +// fun getChildren() : List } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt index 0c3461f1ca..4043a3f7b4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.space import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult typealias SpaceSummaryQueryParams = RoomSummaryQueryParams @@ -35,6 +36,13 @@ interface SpaceService { */ fun getSpace(spaceId: String): Space? + /** + * Try to resolve (peek) rooms and subspace in this space. + * Use this call get preview of children of this space, particularly useful to get a + * preview of rooms that you did not join yet. + */ + suspend fun peekSpace(spaceId: String) : SpacePeekResult + /** * Get a live list of space summaries. This list is refreshed as soon as the data changes. * @return the [LiveData] of List[SpaceSummary] @@ -42,4 +50,23 @@ interface SpaceService { fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List + + data class ChildAutoJoinInfo( + val roomIdOrAlias: String, + val viaServers: List = emptyList() + ) + + sealed class JoinSpaceResult { + object Success: JoinSpaceResult() + data class Fail(val error: Throwable?): JoinSpaceResult() + /** Success fully joined the space, but failed to join all or some of it's rooms */ + data class PartialSuccess(val failedRooms: Map) : JoinSpaceResult() + + fun isSuccess() = this is Success || this is PartialSuccess + } + + suspend fun joinSpace(spaceIdOrAlias: String, + reason: String? = null, + viaServers: List = emptyList(), + autoJoinChild: List) : JoinSpaceResult } 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 index f65318b543..f7bd067c55 100644 --- 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 @@ -35,6 +35,7 @@ data class SpaceChildContent( @Json(name = "via") val via: List? = null, /** * present: true key is included to distinguish from a deleted state event + * Children where present is not present or is not set to true are ignored. */ @Json(name = "present") val present: Boolean? = false, /** 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 8f3445bec3..b7c4246eca 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 @@ -91,6 +91,8 @@ 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 org.matrix.android.sdk.internal.session.space.peeking.DefaultPeekSpaceTask +import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask import retrofit2.Retrofit @Module @@ -236,6 +238,9 @@ internal abstract class RoomModule { @Binds abstract fun bindPeekRoomTask(task: DefaultPeekRoomTask): PeekRoomTask + @Binds + abstract fun bindPeekSpaceTask(task: DefaultPeekSpaceTask): PeekSpaceTask + @Binds abstract fun bindGetEventTask(task: DefaultGetEventTask): GetEventTask } 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 index 4025861caa..b1bcfc7077 100644 --- 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 @@ -44,7 +44,7 @@ internal class RoomRelationshipHelper(private val realm: Realm, .filter { ContentMapper.map(it.root?.content).toModel()?.present == true } .mapNotNull { // ContentMapper.map(it.root?.content).toModel() - it.roomId + it.stateKey } } } 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 f8a3495aa2..7b637cc9e9 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 @@ -167,7 +167,7 @@ internal class RoomSummaryUpdater @Inject constructor( spaceSummaryEntity.children.clear() spaceSummaryEntity.children.addAll( RoomRelationshipHelper(realm, roomId).getDirectChildrenDescriptions() - .map { RoomSummaryEntity.getOrCreate(realm, roomId) } + .map { RoomSummaryEntity.getOrCreate(realm, it) } ) } } 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 index ae71ee5cf2..ebe845572d 100644 --- 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 @@ -35,4 +35,12 @@ class DefaultSpace(private val room: Room) : Space { body = SpaceChildContent(present = true).toContent() ) } + +// override fun getChildren(): List { +// // asRoom().getStateEvents(setOf(EventType.STATE_SPACE_CHILD)).mapNotNull { +// // // statekeys are the roomIds +// // +// // } +// return emptyList() +// } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index 3d9e7d7764..4118d74604 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -32,6 +32,8 @@ 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.space.peeking.PeekSpaceTask +import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask import org.matrix.android.sdk.internal.task.TaskExecutor import javax.inject.Inject @@ -46,6 +48,7 @@ internal class DefaultSpaceService @Inject constructor( private val deleteRoomAliasTask: DeleteRoomAliasTask, private val roomGetter: RoomGetter, private val spaceSummaryDataSource: SpaceSummaryDataSource, + private val peekSpaceTask: PeekSpaceTask, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, private val taskExecutor: TaskExecutor ) : SpaceService { @@ -67,4 +70,31 @@ internal class DefaultSpaceService @Inject constructor( override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List { return spaceSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams) } + + override suspend fun peekSpace(spaceId: String): SpacePeekResult { + return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId)) + } + + override suspend fun joinSpace(spaceIdOrAlias: String, reason: String?, viaServers: List, autoJoinChild: List): SpaceService.JoinSpaceResult { + try { + joinRoomTask.execute(JoinRoomTask.Params(spaceIdOrAlias, reason, viaServers)) + val childJoinFailures = mutableMapOf() + autoJoinChild.forEach { info -> + // TODO what if the child is it self a subspace with some default children? + try { + joinRoomTask.execute(JoinRoomTask.Params(info.roomIdOrAlias, null, info.viaServers)) + } catch (failure: Throwable) { + // TODO, i could already be a member of this room, handle that as it should not be an error in this context + childJoinFailures[info.roomIdOrAlias] = failure + } + } + return if (childJoinFailures.isEmpty()) { + SpaceService.JoinSpaceResult.Success + } else { + SpaceService.JoinSpaceResult.PartialSuccess(childJoinFailures) + } + } catch (throwable: Throwable) { + return SpaceService.JoinSpaceResult.Fail(throwable) + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt new file mode 100644 index 0000000000..826be0b3aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt @@ -0,0 +1,141 @@ +/* + * 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 org.matrix.android.sdk.internal.session.space.peeking + +import org.matrix.android.sdk.api.session.events.model.Event +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.room.model.RoomType +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.api.session.room.peeking.PeekResult +import org.matrix.android.sdk.api.session.space.model.SpaceChildContent +import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask +import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask +import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber +import javax.inject.Inject + +internal interface PeekSpaceTask : Task { + data class Params( + val roomIdOrAlias: String, + // A depth limit as a simple protection against cycles + val maxDepth: Int = 4 + ) +} + +internal class DefaultPeekSpaceTask @Inject constructor( + private val peekRoomTask: PeekRoomTask, + private val resolveRoomStateTask: ResolveRoomStateTask +) : PeekSpaceTask { + + override suspend fun execute(params: PeekSpaceTask.Params): SpacePeekResult { + val peekResult = peekRoomTask.execute(PeekRoomTask.Params(params.roomIdOrAlias)) + val roomResult = peekResult as? PeekResult.Success ?: return SpacePeekResult.FailedToResolve(params.roomIdOrAlias, peekResult) + + // check the room type + // kind of duplicate cause we already did it in Peek? could we pass on the result?? + val stateEvents = try { + resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomResult.roomId)) + } catch (failure: Throwable) { + return SpacePeekResult.FailedToResolve(params.roomIdOrAlias, peekResult) + } + val isSpace = stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_CREATE && it.stateKey == "" } + ?.let { it.content?.toModel()?.type } == RoomType.SPACE + + if (!isSpace) return SpacePeekResult.NotSpaceType(params.roomIdOrAlias) + + val children = peekChildren(stateEvents, 0, params.maxDepth) + + return SpacePeekResult.Success( + SpacePeekSummary( + params.roomIdOrAlias, + peekResult, + children + ) + ) + } + + private suspend fun peekChildren(stateEvents: List, depth: Int, maxDepth: Int): List { + if (depth >= maxDepth) return emptyList() + val childRoomsIds = stateEvents + .filter { + it.type == EventType.STATE_SPACE_CHILD && !it.stateKey.isNullOrEmpty() + // Children where present is not present or is not set to true are ignored. + && it.content?.toModel()?.present == true + } + .map { it.stateKey to it.content?.toModel() } + + Timber.v("## SPACE_PEEK: found ${childRoomsIds.size} present children") + + val spaceChildResults = mutableListOf() + childRoomsIds.forEach { entry -> + + Timber.v("## SPACE_PEEK: peeking child $entry") + // peek each child + val childId = entry.first ?: return@forEach + try { + val childPeek = peekRoomTask.execute(PeekRoomTask.Params(childId)) + + val childStateEvents = resolveRoomStateTask.execute(ResolveRoomStateTask.Params(childId)) + val createContent = childStateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_CREATE && it.stateKey == "" } + ?.let { it.content?.toModel() } + + if (!childPeek.isSuccess() || createContent == null) { + Timber.v("## SPACE_PEEK: cannot peek child $entry") + // can't peek :/ + spaceChildResults.add( + SpaceChildPeekResult( + childId, childPeek, entry.second?.default, entry.second?.order + ) + ) + // continue to next child + return@forEach + } + val type = createContent.type + if (type == RoomType.SPACE) { + Timber.v("## SPACE_PEEK: subspace child $entry") + spaceChildResults.add( + SpaceSubChildPeekResult( + childId, + childPeek, + entry.second?.default, + entry.second?.order, + peekChildren(childStateEvents, depth + 1, maxDepth) + ) + ) + } else if (type == RoomType.MESSAGING || type == null) { + Timber.v("## SPACE_PEEK: room child $entry") + spaceChildResults.add( + SpaceChildPeekResult( + childId, childPeek, entry.second?.default, entry.second?.order + ) + ) + } else { + // ignore for now? + } + + // let's check child info + } catch (failure: Throwable) { + // can this happen? + Timber.e(failure, "## Failed to resolve space child") + } + } + return spaceChildResults + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt new file mode 100644 index 0000000000..63eed2a6c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt @@ -0,0 +1,55 @@ +/* + * 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 org.matrix.android.sdk.internal.session.space.peeking + +import org.matrix.android.sdk.api.session.room.peeking.PeekResult + +data class SpacePeekSummary( + val idOrAlias: String, + val roomPeekResult: PeekResult.Success, + val children: List +) + +interface ISpaceChild { + val id: String + val roomPeekResult: PeekResult + val default: Boolean? + val order: String? +} + +data class SpaceChildPeekResult( + override val id: String, + override val roomPeekResult: PeekResult, + override val default: Boolean? = null, + override val order: String? = null +) : ISpaceChild + +data class SpaceSubChildPeekResult( + override val id: String, + override val roomPeekResult: PeekResult, + override val default: Boolean?, + override val order: String?, + val children: List +) : ISpaceChild + +sealed class SpacePeekResult { + abstract class SpacePeekError : SpacePeekResult() + data class FailedToResolve(val spaceId: String, val roomPeekResult: PeekResult) : SpacePeekError() + data class NotSpaceType(val spaceId: String) : SpacePeekError() + + data class Success(val summary: SpacePeekSummary): SpacePeekResult() +} diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 07606d315c..9a1f2e6dfd 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -271,7 +271,8 @@ - + + ", R.string.command_confetti), SNOW("/snow", "", R.string.command_snow), - CREATE_SPACE("/createspace", " *", R.string.command_description_create_space); + CREATE_SPACE("/createspace", " *", R.string.command_description_create_space), + ADD_TO_SPACE("/addToSpace", "spaceId", 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 57466ddf98..fe5707ec45 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 @@ -296,7 +296,7 @@ object CommandParser { val message = textMessage.substring(Command.CONFETTI.command.length).trim() ParsedCommand.SendChatEffect(ChatEffect.CONFETTI, message) } - Command.SNOW.command -> { + Command.SNOW.command -> { val message = textMessage.substring(Command.SNOW.command.length).trim() ParsedCommand.SendChatEffect(ChatEffect.SNOW, message) } @@ -312,6 +312,12 @@ object CommandParser { ) } } + Command.ADD_TO_SPACE.command -> { + val rawCommand = textMessage.substring(Command.ADD_TO_SPACE.command.length).trim() + ParsedCommand.AddToSpace( + rawCommand + ) + } 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 1017b29234..99b0ae7889 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 @@ -58,4 +58,5 @@ sealed class ParsedCommand { object DiscardSession : ParsedCommand() class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand() class CreateSpace(val name: String, val invitees: List) : ParsedCommand() + class AddToSpace(val spaceId: String) : ParsedCommand() } 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 index 4c783cb2d4..7091c0b86c 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/SpaceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/SpaceListFragment.kt @@ -59,7 +59,8 @@ class SpaceListFragment @Inject constructor( views.groupListView.configureWith(spaceController) viewModel.observeViewEvents { when (it) { - is SpaceListViewEvents.OpenSpaceSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup) + is SpaceListViewEvents.OpenSpaceSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenSpacePreview(it.id)) + is SpaceListViewEvents.OpenSpace -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup) }.exhaustive } } 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 index c7a27266fd..dab2cbceae 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryController.kt @@ -18,9 +18,13 @@ package im.vector.app.features.grouplist import com.airbnb.epoxy.EpoxyController +import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.core.ui.list.genericItemHeader import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.spaces.SpaceListViewState +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -50,24 +54,57 @@ class SpaceSummaryController @Inject constructor( 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) } + // show invites on top + + summaries.filter { it.roomSummary.membership == Membership.INVITE } + .let { invites -> + if (invites.isNotEmpty()) { + genericItemHeader { + id("invites") + text(stringProvider.getString(R.string.spaces_invited_header)) + } + invites.forEach { + spaceSummaryItem { + avatarRenderer(avatarRenderer) + id(it.spaceId) + matrixItem(it.toMatrixItem()) + selected(false) + listener { callback?.onSpaceSelected(it) } + } + } + genericFooterItem { + id("invite_space") + text("") + } + } } - } else { - spaceSummaryItem { - avatarRenderer(avatarRenderer) - id(groupSummary.spaceId) - matrixItem(groupSummary.toMatrixItem()) - selected(isSelected) - listener { callback?.onSpaceSelected(groupSummary) } - } - } + + genericItemHeader { + id("spaces") + text(stringProvider.getString(R.string.spaces_header)) } + + summaries + .filter { it.roomSummary.membership == Membership.JOIN } + .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 { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 60a8836be5..138ffc26f4 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -54,6 +54,7 @@ import im.vector.app.features.popup.VerificationVectorAlert import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity +import im.vector.app.features.spaces.SpacePreviewActivity import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import im.vector.app.features.workers.signout.ServerBackupStatusViewState @@ -141,6 +142,9 @@ class HomeActivity : views.drawerLayout.closeDrawer(GravityCompat.START) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true) } + is HomeActivitySharedAction.OpenSpacePreview -> { + startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId)) + } }.exhaustive } .disposeOnDestroy() diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt index 52b3c58785..f72354465b 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt @@ -25,4 +25,5 @@ sealed class HomeActivitySharedAction : VectorSharedAction { object OpenDrawer : HomeActivitySharedAction() object CloseDrawer : HomeActivitySharedAction() object OpenGroup : HomeActivitySharedAction() + data class OpenSpacePreview(val spaceId: String) : HomeActivitySharedAction() } 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 bdf38719e2..785449236c 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 @@ -839,6 +839,17 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) popDraft() } + is ParsedCommand.AddToSpace -> { + viewModelScope.launch(Dispatchers.IO) { + try { + session.spaceService().getSpace(slashCommandResult.spaceId)?.addRoom(room.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/spaces/SpacePreviewActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt new file mode 100644 index 0000000000..dacde8846c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.airbnb.mvrx.MvRx +import im.vector.app.R +import im.vector.app.core.extensions.commitTransaction +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivitySimpleBinding +import im.vector.app.features.spaces.preview.SpacePreviewArgs +import im.vector.app.features.spaces.preview.SpacePreviewFragment + +class SpacePreviewActivity : VectorBaseActivity() { + + lateinit var sharedActionViewModel: SpacePreviewSharedActionViewModel + + override fun getBinding(): ActivitySimpleBinding = ActivitySimpleBinding.inflate(layoutInflater) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedActionViewModel = viewModelProvider.get(SpacePreviewSharedActionViewModel::class.java) + sharedActionViewModel + .observe() + .subscribe { action -> + when (action) { + SpacePreviewSharedAction.DismissAction -> finish() + SpacePreviewSharedAction.ShowModalLoading -> showWaitingView() + SpacePreviewSharedAction.HideModalLoading -> hideWaitingView() + is SpacePreviewSharedAction.ShowErrorMessage -> action.error?.let { showSnackbar(it) } + } + }.disposeOnDestroy() + + if (isFirstCreation()) { + val simpleName = SpacePreviewFragment::class.java.simpleName + val args = intent?.getParcelableExtra(MvRx.KEY_ARG) + if (supportFragmentManager.findFragmentByTag(simpleName) == null) { + supportFragmentManager.commitTransaction { + replace(R.id.simpleFragmentContainer, + SpacePreviewFragment::class.java, + Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) }, + simpleName + ) + } + } + } + } + + companion object { + fun newIntent(context: Context, spaceIdOrAlias: String): Intent { + return Intent(context, SpacePreviewActivity::class.java).apply { + putExtra(MvRx.KEY_ARG, SpacePreviewArgs(spaceIdOrAlias)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewSharedActionViewModel.kt new file mode 100644 index 0000000000..058b1a275b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewSharedActionViewModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces + +import im.vector.app.core.platform.VectorSharedAction +import im.vector.app.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +sealed class SpacePreviewSharedAction : VectorSharedAction { + object DismissAction : SpacePreviewSharedAction() + object ShowModalLoading : SpacePreviewSharedAction() + object HideModalLoading : SpacePreviewSharedAction() + data class ShowErrorMessage(val error: String? = null) : SpacePreviewSharedAction() +} + +class SpacePreviewSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() 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 index 5daedcc984..f0d8ae30f7 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt @@ -53,7 +53,8 @@ sealed class SpaceListAction : VectorViewModelAction { * Transient events for group list screen */ sealed class SpaceListViewEvents : VectorViewEvents { - object OpenSpaceSummary : SpaceListViewEvents() + object OpenSpace : SpaceListViewEvents() + data class OpenSpaceSummary(val id: String) : SpaceListViewEvents() } data class SpaceListViewState( @@ -94,7 +95,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp // 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) + _viewEvents.post(SpaceListViewEvents.OpenSpace) } val optionGroup = Option.just(spaceSummary) selectedSpaceDataSource.post(optionGroup) @@ -116,20 +117,27 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp // 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() + + if (state.selectedSpace?.roomSummary?.membership == Membership.INVITE) { + _viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(state.selectedSpace.roomSummary.roomId)) +// viewModelScope.launch(Dispatchers.IO) { +// tryOrNull { session.spaceService().peekSpace(action.spaceSummary.spaceId) }.let { +// Timber.d("PEEK RESULT/ $it") // } // } - setState { copy(selectedSpace = action.spaceSummary) } + } else { + if (state.selectedSpace?.spaceId != action.spaceSummary.spaceId) { +// state.selectedSpace?.let { +// selectedSpaceDataSource.post(Option.just(state.selectedSpace)) +// } + setState { copy(selectedSpace = action.spaceSummary) } + } } } private fun observeGroupSummaries() { val roomSummaryQueryParams = roomSummaryQueryParams() { - memberships = listOf(Membership.JOIN) + memberships = listOf(Membership.JOIN, Membership.INVITE) displayName = QueryStringValue.IsNotEmpty excludeType = listOf(RoomType.MESSAGING, null) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/RoomChildItem.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/RoomChildItem.kt new file mode 100644 index 0000000000..bf28618c6c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/RoomChildItem.kt @@ -0,0 +1,79 @@ +/* + * 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.preview + +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.extensions.setTextOrHide +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.MatrixItem + +@EpoxyModelClass(layout = R.layout.item_space_roomchild) +abstract class RoomChildItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var roomId: String + + @EpoxyAttribute + lateinit var title: String + + @EpoxyAttribute + var topic: String? = null + + @EpoxyAttribute + lateinit var memberCount: String + + @EpoxyAttribute + var avatarUrl: String? = null + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + var depth: Int = 0 + + override fun bind(holder: Holder) { + super.bind(holder) + holder.roomNameText.text = title + holder.roomTopicText.setTextOrHide(topic) + holder.memberCountText.text = memberCount + + avatarRenderer.render( + MatrixItem.RoomItem(roomId, title, avatarUrl), + holder.avatarImageView + ) + holder.tabView.tabDepth = depth + } + + override fun unbind(holder: Holder) { + avatarRenderer.clear(holder.avatarImageView) + super.unbind(holder) + } + + class Holder : VectorEpoxyHolder() { + val avatarImageView by bind(R.id.childRoomAvatar) + val roomNameText by bind(R.id.childRoomName) + val roomTopicText by bind(R.id.childRoomTopic) + val memberCountText by bind(R.id.spaceChildMemberCountText) + val tabView by bind(R.id.spaceChildTabView) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt new file mode 100644 index 0000000000..651411b2fe --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt @@ -0,0 +1,117 @@ +/* + * 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.preview + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.core.ui.list.genericItemHeader +import im.vector.app.core.utils.TextUtils +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.session.room.peeking.PeekResult +import org.matrix.android.sdk.internal.session.space.peeking.ISpaceChild +import org.matrix.android.sdk.internal.session.space.peeking.SpaceChildPeekResult +import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult +import org.matrix.android.sdk.internal.session.space.peeking.SpaceSubChildPeekResult +import javax.inject.Inject + +class SpacePreviewController @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider +) : TypedEpoxyController() { + + interface InteractionListener + + var interactionListener: InteractionListener? = null + + override fun buildModels(data: SpacePreviewState?) { + val result: SpacePeekResult = data?.peekResult?.invoke() ?: return + + when (result) { + is SpacePeekResult.SpacePeekError -> { + genericFooterItem { + id("failed") + // TODO + text("Failed to resolve") + } + } + is SpacePeekResult.Success -> { + // add summary info + val memberCount = result.summary.roomPeekResult.numJoinedMembers ?: 0 + + spaceTopSummaryItem { + id("info") + formattedMemberCount(stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)) + topic(result.summary.roomPeekResult.topic ?: "") + } + + genericItemHeader { + id("header_rooms") + text(stringProvider.getString(R.string.rooms)) + } + + buildChildren(result.summary.children, 0) + } + } + } + + private fun buildChildren(children: List, depth: Int) { + children.forEach { child -> + when (child) { + is SpaceSubChildPeekResult -> { + when (val roomPeekResult = child.roomPeekResult) { + is PeekResult.Success -> { + subSpaceItem { + id(roomPeekResult.roomId) + roomId(roomPeekResult.roomId) + title(roomPeekResult.name) + depth(depth) + avatarUrl(roomPeekResult.avatarUrl) + avatarRenderer(avatarRenderer) + } + buildChildren(child.children, depth + 1) + } + else -> { + // ?? TODO + } + } + } + is SpaceChildPeekResult -> { + // We have to check if the peek result was success + when (val roomPeekResult = child.roomPeekResult) { + is PeekResult.Success -> { + roomChildItem { + id(child.id) + depth(depth) + roomId(roomPeekResult.roomId) + title(roomPeekResult.name ?: "") + topic(roomPeekResult.topic ?: "") + avatarUrl(roomPeekResult.avatarUrl) + memberCount(TextUtils.formatCountToShortDecimal(roomPeekResult.numJoinedMembers ?: 0)) + avatarRenderer(avatarRenderer) + } + } + else -> { + // What to do here? + } + } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt new file mode 100644 index 0000000000..fcf961f23a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt @@ -0,0 +1,165 @@ +/* + * 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.preview + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.appcompat.navigationClicks +import im.vector.app.R +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentSpacePreviewBinding +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.spaces.SpacePreviewSharedAction +import im.vector.app.features.spaces.SpacePreviewSharedActionViewModel +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@Parcelize +data class SpacePreviewArgs( + val idOrAlias: String +) : Parcelable + +class SpacePreviewFragment @Inject constructor( + private val viewModelFactory: SpacePreviewViewModel.Factory, + private val avatarRenderer: AvatarRenderer, + private val epoxyController: SpacePreviewController +) : VectorBaseFragment(), SpacePreviewViewModel.Factory { + + private val viewModel by fragmentViewModel(SpacePreviewViewModel::class) + lateinit var sharedActionViewModel: SpacePreviewSharedActionViewModel + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSpacePreviewBinding { + return FragmentSpacePreviewBinding.inflate(inflater, container, false) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedActionViewModel = activityViewModelProvider.get(SpacePreviewSharedActionViewModel::class.java) + } + + override fun create(initialState: SpacePreviewState) = viewModelFactory.create(initialState) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.observeViewEvents { + handleViewEvents(it) + } + + views.roomPreviewNoPreviewToolbar.navigationClicks() + .throttleFirst(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { sharedActionViewModel.post(SpacePreviewSharedAction.DismissAction) } + .disposeOnDestroyView() + + views.spacePreviewRecyclerView.configureWith(epoxyController) + + views.spacePreviewAcceptInviteButton.debouncedClicks { + viewModel.handle(SpacePreviewViewAction.AcceptInvite) + } + + views.spacePreviewDeclineInviteButton.debouncedClicks { + viewModel.handle(SpacePreviewViewAction.DismissInvite) + } + } + + override fun onDestroyView() { + views.spacePreviewRecyclerView.cleanup() + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { + when (it.peekResult) { + is Uninitialized, + is Loading -> { + views.spacePreviewPeekingProgress.isVisible = true + views.spacePreviewButtonBar.isVisible = true + views.spacePreviewAcceptInviteButton.isEnabled = false + views.spacePreviewDeclineInviteButton.isEnabled = false + } + is Fail -> { + views.spacePreviewPeekingProgress.isVisible = false + views.spacePreviewButtonBar.isVisible = false + } + is Success -> { + views.spacePreviewPeekingProgress.isVisible = false + views.spacePreviewButtonBar.isVisible = true + views.spacePreviewAcceptInviteButton.isEnabled = true + views.spacePreviewDeclineInviteButton.isEnabled = true + epoxyController.setData(it) + } + } + updateToolbar(it) + } + + private fun handleViewEvents(viewEvents: SpacePreviewViewEvents) { + when (viewEvents) { + SpacePreviewViewEvents.Dismiss -> { + } + SpacePreviewViewEvents.StartJoining -> { + sharedActionViewModel.post(SpacePreviewSharedAction.ShowModalLoading) + } + SpacePreviewViewEvents.JoinSuccess -> { + sharedActionViewModel.post(SpacePreviewSharedAction.HideModalLoading) + sharedActionViewModel.post(SpacePreviewSharedAction.DismissAction) + } + is SpacePreviewViewEvents.JoinFailure -> { + sharedActionViewModel.post(SpacePreviewSharedAction.HideModalLoading) + sharedActionViewModel.post(SpacePreviewSharedAction.ShowErrorMessage(viewEvents.message ?: getString(R.string.matrix_error))) + } + } + } + + private fun updateToolbar(spacePreviewState: SpacePreviewState) { + when (val preview = spacePreviewState.peekResult.invoke()) { + is SpacePeekResult.Success -> { + val roomPeekResult = preview.summary.roomPeekResult + val mxItem = MatrixItem.RoomItem(roomPeekResult.roomId, roomPeekResult.name, roomPeekResult.avatarUrl) + avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar) + views.roomPreviewNoPreviewToolbarTitle.text = roomPeekResult.name + } + is SpacePeekResult.SpacePeekError, + null -> { + // what to do here? + val mxItem = MatrixItem.RoomItem(spacePreviewState.idOrAlias, spacePreviewState.name, spacePreviewState.avatarUrl) + avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar) + views.roomPreviewNoPreviewToolbarTitle.text = spacePreviewState.name + } + } + } + + override fun onStart() { + super.onStart() + viewModel.handle(SpacePreviewViewAction.ViewReady) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt new file mode 100644 index 0000000000..41d94e8c9d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.preview + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult + +data class SpacePreviewState( + val idOrAlias: String, + val name: String? = null, + val avatarUrl: String? = null, + val peekResult: Async = Uninitialized +) : MvRxState { + constructor(args: SpacePreviewArgs) : this(idOrAlias = args.idOrAlias) +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewAction.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewAction.kt new file mode 100644 index 0000000000..6426b89d55 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.preview + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class SpacePreviewViewAction : VectorViewModelAction { + object ViewReady : SpacePreviewViewAction() + object AcceptInvite : SpacePreviewViewAction() + object DismissInvite : SpacePreviewViewAction() +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt new file mode 100644 index 0000000000..04645e59ad --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.preview + +import im.vector.app.core.platform.VectorViewEvents + +sealed class SpacePreviewViewEvents : VectorViewEvents { + object Dismiss: SpacePreviewViewEvents() + object StartJoining: SpacePreviewViewEvents() + object JoinSuccess: SpacePreviewViewEvents() + data class JoinFailure(val message: String?): SpacePreviewViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt new file mode 100644 index 0000000000..6986db18aa --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt @@ -0,0 +1,135 @@ +/* + * 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.preview + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.peeking.PeekResult +import org.matrix.android.sdk.api.session.space.SpaceService +import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult + +class SpacePreviewViewModel @AssistedInject constructor( + @Assisted private val initialState: SpacePreviewState, + private val session: Session +) : VectorViewModel(initialState) { + + private var initialized = false + + init { + // do we have some things in cache? + session.getRoomSummary(initialState.idOrAlias)?.let { + setState { + copy(name = it.name, avatarUrl = it.avatarUrl) + } + } + } + + @AssistedInject.Factory + interface Factory { + fun create(initialState: SpacePreviewState): SpacePreviewViewModel + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: SpacePreviewState): SpacePreviewViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + override fun handle(action: SpacePreviewViewAction) { + when (action) { + SpacePreviewViewAction.ViewReady -> handleReady() + SpacePreviewViewAction.AcceptInvite -> handleAcceptInvite() + SpacePreviewViewAction.DismissInvite -> handleDismissInvite() + } + } + + private fun handleDismissInvite() { + TODO("Not yet implemented") + } + + private fun handleAcceptInvite() = withState { state -> + // Here we need to join the space himself as well as the default rooms in that space + val spaceInfo = state.peekResult.invoke() as? SpacePeekResult.Success + + // TODO if we have no summary, we cannot find auto join rooms... + // So maybe we should trigger a retry on summary after the join? + val spaceVia = (spaceInfo?.summary?.roomPeekResult as? PeekResult.Success)?.viaServers ?: emptyList() + val autoJoinChildren = spaceInfo?.summary?.children + ?.filter { it.default == true } + ?.map { + SpaceService.ChildAutoJoinInfo( + it.id, + // via servers + (it.roomPeekResult as? PeekResult.Success)?.viaServers ?: emptyList() + ) + } ?: emptyList() + + // trigger modal loading + _viewEvents.post(SpacePreviewViewEvents.StartJoining) + viewModelScope.launch(Dispatchers.IO) { + val joinResult = session.spaceService().joinSpace(spaceInfo?.summary?.idOrAlias ?: initialState.idOrAlias, null, spaceVia, autoJoinChildren) + when (joinResult) { + SpaceService.JoinSpaceResult.Success, + is SpaceService.JoinSpaceResult.PartialSuccess -> { + // For now we don't handle partial success, it's just success + _viewEvents.post(SpacePreviewViewEvents.JoinSuccess) + } + is SpaceService.JoinSpaceResult.Fail -> { + _viewEvents.post(SpacePreviewViewEvents.JoinFailure(joinResult.error?.toString())) + } + } + } + } + + private fun handleReady() { + if (!initialized) { + initialized = true + // peek for the room + setState { + copy(peekResult = Loading()) + } + viewModelScope.launch(Dispatchers.IO) { + try { + val result = session.spaceService().peekSpace(initialState.idOrAlias) + setState { + copy(peekResult = Success(result)) + } + } catch (failure: Throwable) { + setState { + copy(peekResult = Fail(failure)) + } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTabView.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTabView.kt new file mode 100644 index 0000000000..675e7070e7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTabView.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.preview + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import im.vector.app.R + +class SpaceTabView constructor(context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0) + : LinearLayout(context, attrs, defStyleAttr) { + + constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) {} + constructor(context: Context) : this(context, null, 0) {} + + var tabDepth = 0 + set(value) { + if (field != value) { + field = value + setUpView() + } + } + + init { + setUpView() + } + + private fun setUpView() { + // remove children + removeAllViews() + for (i in 0 until tabDepth) { + inflate(context, R.layout.item_space_tab, this) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTopSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTopSummaryItem.kt new file mode 100644 index 0000000000..c357fb14b3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTopSummaryItem.kt @@ -0,0 +1,46 @@ +/* + * 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.preview + +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.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_space_top_summary) +abstract class SpaceTopSummaryItem : VectorEpoxyModel() { + + @EpoxyAttribute + var topic: String? = null + + @EpoxyAttribute + lateinit var formattedMemberCount: String + + override fun bind(holder: Holder) { + super.bind(holder) + holder.spaceTopicText.setTextOrHide(topic) + holder.memberCountText.text = formattedMemberCount + } + + class Holder : VectorEpoxyHolder() { + val memberCountText by bind(R.id.spaceSummaryMemberCountText) + val spaceTopicText by bind(R.id.spaceSummaryTopic) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt new file mode 100644 index 0000000000..367a81fe5a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt @@ -0,0 +1,68 @@ +/* + * 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.preview + +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.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.MatrixItem + +@EpoxyModelClass(layout = R.layout.item_space_subspace) +abstract class SubSpaceItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var roomId: String + + @EpoxyAttribute + lateinit var title: String + + @EpoxyAttribute + var avatarUrl: String? = null + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + var depth: Int = 0 + + override fun bind(holder: Holder) { + super.bind(holder) + holder.nameText.text = title + + avatarRenderer.renderSpace( + MatrixItem.RoomItem(roomId, title, avatarUrl), + holder.avatarImageView + ) + holder.tabView.tabDepth = depth + } + + override fun unbind(holder: Holder) { + avatarRenderer.clear(holder.avatarImageView) + super.unbind(holder) + } + + class Holder : VectorEpoxyHolder() { + val avatarImageView by bind(R.id.childSpaceAvatar) + val nameText by bind(R.id.childSpaceName) + val tabView by bind(R.id.childSpaceTab) + } +} diff --git a/vector/src/main/res/layout/fragment_space_preview.xml b/vector/src/main/res/layout/fragment_space_preview.xml new file mode 100644 index 0000000000..257797548e --- /dev/null +++ b/vector/src/main/res/layout/fragment_space_preview.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_space_roomchild.xml b/vector/src/main/res/layout/item_space_roomchild.xml new file mode 100644 index 0000000000..0fdbd833f4 --- /dev/null +++ b/vector/src/main/res/layout/item_space_roomchild.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_space_subspace.xml b/vector/src/main/res/layout/item_space_subspace.xml new file mode 100644 index 0000000000..ac654dc2b3 --- /dev/null +++ b/vector/src/main/res/layout/item_space_subspace.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_space_tab.xml b/vector/src/main/res/layout/item_space_tab.xml new file mode 100644 index 0000000000..ea08fabea3 --- /dev/null +++ b/vector/src/main/res/layout/item_space_tab.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/vector/src/main/res/layout/item_space_top_summary.xml b/vector/src/main/res/layout/item_space_top_summary.xml new file mode 100644 index 0000000000..e4e2bbdd76 --- /dev/null +++ b/vector/src/main/res/layout/item_space_top_summary.xml @@ -0,0 +1,46 @@ + + + + + + + + + \ 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 c9cb4729f7..b296c4e5ea 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -508,6 +508,9 @@ Communities No groups + Invites + Spaces + Send logs Send crash logs Send key share requests history From ab4f2429c47a1d239582262f7a401b2cbde07021 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 8 Jan 2021 09:07:27 +0100 Subject: [PATCH 03/97] Use unstable prefixes --- .../android/sdk/api/session/events/model/EventType.kt | 4 ++-- .../matrix/android/sdk/api/session/room/model/RoomType.kt | 4 ++-- .../sdk/api/session/room/model/create/RoomCreateContent.kt | 2 +- .../sdk/internal/session/space/peeking/PeekSpaceTask.kt | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) 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 4be8eea856..99934d3d95 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,8 +51,8 @@ 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" -// const val STATE_SPACE_CHILD = "org.matrix.msc1772.space" +// const val STATE_SPACE_CHILD = "m.space.child" + const val STATE_SPACE_CHILD = "org.matrix.msc1772.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/model/RoomType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomType.kt index 3958d45d0b..b4932494f2 100644 --- 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 @@ -18,6 +18,6 @@ package org.matrix.android.sdk.api.session.room.model object RoomType { - const val SPACE = "m.space" - const val MESSAGING = "m.messaging" + const val SPACE = "org.matrix.msc1772.space" // "m.space" +// const val MESSAGING = "org.matrix.msc1840.messaging" } 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 52e5c0e9c7..f9d40d5652 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 @@ -28,5 +28,5 @@ data class RoomCreateContent( @Json(name = "room_version") val roomVersion: String? = null, @Json(name = "predecessor") val predecessor: Predecessor? = null, // Defines the room type, see #RoomType (user extensible) - @Json(name = "type") val type: String? = null + @Json(name = "org.matrix.msc1772.type") val type: String? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt index 826be0b3aa..a2be75a232 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt @@ -119,15 +119,15 @@ internal class DefaultPeekSpaceTask @Inject constructor( peekChildren(childStateEvents, depth + 1, maxDepth) ) ) - } else if (type == RoomType.MESSAGING || type == null) { + } else + /** if (type == RoomType.MESSAGING || type == null)*/ + { Timber.v("## SPACE_PEEK: room child $entry") spaceChildResults.add( SpaceChildPeekResult( childId, childPeek, entry.second?.default, entry.second?.order ) ) - } else { - // ignore for now? } // let's check child info From e2578a29ed905ffe9095e663d51c325d96f14e9e Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 12 Jan 2021 11:40:09 +0100 Subject: [PATCH 04/97] Basic space join / use tmp msc id / db model update --- .../api/session/room/model/SpaceChildInfo.kt | 25 ++++ .../room/model/create/CreateRoomParams.kt | 3 +- .../sdk/api/session/space/SpaceSummary.kt | 3 +- .../session/space/model/SpaceChildContent.kt | 2 +- .../sdk/internal/database/RealmQueryLatch.kt | 2 + .../database/RealmSessionStoreMigration.kt | 11 +- .../database/mapper/SpaceSummaryMapper.kt | 9 +- .../database/model/SessionRealmModule.kt | 3 +- .../database/model/SpaceChildInfoEntity.kt | 38 ++++++ .../database/model/SpaceSummaryEntity.kt | 2 +- .../sdk/internal/session/room/RoomModule.kt | 5 + .../relationship/RoomRelationshipHelper.kt | 24 +++- .../room/summary/RoomSummaryUpdater.kt | 19 ++- .../session/space/DefaultSpaceService.kt | 35 +++--- .../internal/session/space/JoinSpaceTask.kt | 116 ++++++++++++++++++ .../session/space/peeking/SpacePeekResult.kt | 2 +- .../internal/session/sync/RoomSyncHandler.kt | 1 + vector/src/main/AndroidManifest.xml | 1 + .../im/vector/app/core/di/FragmentModule.kt | 2 +- .../app/features/home/HomeDrawerFragment.kt | 2 +- .../features/spaces/SpaceExploreActivity.kt | 73 +++++++++++ .../SpaceListFragment.kt | 10 +- .../SpaceSummaryController.kt | 10 +- .../{grouplist => spaces}/SpaceSummaryItem.kt | 2 +- .../features/spaces/SpacesListViewModel.kt | 13 +- .../explore/SpaceDirectoryController.kt | 53 ++++++++ .../spaces/explore/SpaceDirectoryFragment.kt | 35 ++++++ .../spaces/explore/SpaceDirectoryViewModel.kt | 101 +++++++++++++++ 28 files changed, 549 insertions(+), 53 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt rename vector/src/main/java/im/vector/app/features/{grouplist => spaces}/SpaceListFragment.kt (91%) rename vector/src/main/java/im/vector/app/features/{grouplist => spaces}/SpaceSummaryController.kt (95%) rename vector/src/main/java/im/vector/app/features/{grouplist => spaces}/SpaceSummaryItem.kt (98%) create mode 100644 vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt new file mode 100644 index 0000000000..04f3310a77 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt @@ -0,0 +1,25 @@ +/* + * 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 + +data class SpaceChildInfo( + val roomSummary: IRoomSummary?, + val present: Boolean, + val order: String?, + val autoJoin: Boolean, + val viaServers: List +) 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 6009649314..880854da58 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,7 +21,6 @@ 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 @@ -112,7 +111,7 @@ open class CreateRoomParams { } } - var roomType: String? = RoomType.MESSAGING + var roomType: String? = null // RoomType.MESSAGING set(value) { field = value if (value != null) { 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 index d2be2f18f1..1473ed7a96 100644 --- 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 @@ -18,9 +18,10 @@ 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 +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo data class SpaceSummary( val spaceId: String, val roomSummary: RoomSummary, - val children: List + 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 index f7bd067c55..e31ff5af5c 100644 --- 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 @@ -49,5 +49,5 @@ data class SpaceChildContent( * 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 + @Json(name = "default") val default: Boolean? = false ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmQueryLatch.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmQueryLatch.kt index c9c797304a..04f47bfccd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmQueryLatch.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmQueryLatch.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout +import timber.log.Timber internal suspend fun awaitNotEmptyResult(realmConfiguration: RealmConfiguration, timeoutMillis: Long, @@ -40,6 +41,7 @@ internal suspend fun awaitNotEmptyResult(realmConfiguration: RealmConfigurat val listener = object : RealmChangeListener> { override fun onChange(it: RealmResults) { + Timber.v("## Space: $it") if (it.isNotEmpty()) { result.removeChangeListener(this) latch.complete(Unit) 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 2c06a4e8f7..27b2b031e4 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,9 +29,9 @@ 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.SpaceChildInfoEntityFields import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields import timber.log.Timber import javax.inject.Inject @@ -207,9 +207,16 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { obj.setString(RoomSummaryEntityFields.ROOM_TYPE, null) } + val spaceChildInfoSchema = realm.schema.create("SpaceChildInfoEntity") + ?.addField(SpaceChildInfoEntityFields.ORDER, String::class.java) + ?.addField(SpaceChildInfoEntityFields.PRESENT, Boolean::class.java) + ?.setNullable(SpaceChildInfoEntityFields.PRESENT, true) + ?.addRealmListField(SpaceChildInfoEntityFields.VIA_SERVERS.`$`, String::class.java) + ?.addRealmObjectField(SpaceChildInfoEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) + 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")!!) + ?.addRealmListField(SpaceSummaryEntityFields.CHILDREN.`$`, spaceChildInfoSchema!!) } } 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 index 9dee99d7fe..7128501a65 100644 --- 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 @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.database.mapper +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity import javax.inject.Inject @@ -27,7 +28,13 @@ internal class SpaceSummaryMapper @Inject constructor(private val roomSummaryMap spaceId = spaceSummaryEntity.spaceId, roomSummary = roomSummaryMapper.map(spaceSummaryEntity.roomSummaryEntity!!), children = spaceSummaryEntity.children.map { - roomSummaryMapper.map(it) + SpaceChildInfo( + roomSummary = it.roomSummaryEntity?.let { rs -> roomSummaryMapper.map(rs) }, + autoJoin = it.autoJoin ?: false, + present = it.present ?: false, + viaServers = it.viaServers.map { it }, + order = it.order + ) } ) } 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 8c5bb8e990..76116be1a8 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 @@ -62,6 +62,7 @@ import io.realm.annotations.RealmModule UserAccountDataEntity::class, ScalarTokenEntity::class, WellknownIntegrationManagerConfigEntity::class, - SpaceSummaryEntity::class + SpaceSummaryEntity::class, + SpaceChildInfoEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt new file mode 100644 index 0000000000..68667c55fc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Decorates room summary with space related information. + */ +internal open class SpaceChildInfoEntity( + var viaServers: RealmList = RealmList(), + // it's an active child of the space if and only if present is not null and true + var present: Boolean? = null, + // Use for alphabetic ordering of this child + var order: String? = null, + // If true, this child should be join when parent is joined + var autoJoin: Boolean? = null, + // link to the actual room (check type to see if it's a subspace) + var roomSummaryEntity: RoomSummaryEntity? = null +) : RealmObject() { + + companion object +} 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 index ca54655022..e63b5b9d55 100644 --- 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 @@ -22,7 +22,7 @@ import io.realm.annotations.PrimaryKey internal open class SpaceSummaryEntity(@PrimaryKey var spaceId: String = "", var roomSummaryEntity: RoomSummaryEntity? = null, - var children: RealmList = RealmList() + var children: RealmList = RealmList() // TODO public / private .. and more ) : RealmObject() { 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 b7c4246eca..8cc7f41d5b 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 @@ -90,7 +90,9 @@ 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.DefaultJoinSpaceTask import org.matrix.android.sdk.internal.session.space.DefaultSpaceService +import org.matrix.android.sdk.internal.session.space.JoinSpaceTask import org.matrix.android.sdk.internal.session.space.peeking.DefaultPeekSpaceTask import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask import retrofit2.Retrofit @@ -241,6 +243,9 @@ internal abstract class RoomModule { @Binds abstract fun bindPeekSpaceTask(task: DefaultPeekSpaceTask): PeekSpaceTask + @Binds + abstract fun bindJoinSpaceTask(task: DefaultJoinSpaceTask): JoinSpaceTask + @Binds abstract fun bindGetEventTask(task: DefaultGetEventTask): GetEventTask } 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 index b1bcfc7077..54b9a17c3c 100644 --- 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 @@ -23,6 +23,7 @@ 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 +import timber.log.Timber /** * Relationship between rooms and spaces @@ -38,13 +39,30 @@ internal class RoomRelationshipHelper(private val realm: Realm, private val roomId: String ) { - fun getDirectChildrenDescriptions(): List { + data class SpaceChildInfo( + val roomId: String, + val present: Boolean, + val order: String?, + val autoJoin: Boolean, + val viaServers: List + ) + + fun getDirectChildrenDescriptions(): List { return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD) .findAll() - .filter { ContentMapper.map(it.root?.content).toModel()?.present == true } +// .filter { ContentMapper.map(it.root?.content).toModel()?.present == true } .mapNotNull { // ContentMapper.map(it.root?.content).toModel() - it.stateKey + ContentMapper.map(it.root?.content).toModel()?.let { scc -> + Timber.d("## Space child desc state event $scc") + SpaceChildInfo( + roomId = it.stateKey, + present = scc.present ?: false, + order = scc.order, + autoJoin = scc.default ?: false, + viaServers = scc.via ?: emptyList() + ) + } } } } 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 7b637cc9e9..11d0ffffde 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 @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.summary import io.realm.Realm +import io.realm.kotlin.createObject import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -38,6 +39,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.SpaceChildInfoEntity 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 @@ -162,13 +164,26 @@ internal class RoomSummaryUpdater @Inject constructor( } if (roomType == RoomType.SPACE) { - val spaceSummaryEntity = SpaceSummaryEntity.getOrCreate(realm, roomId) + Timber.v("## Space: Updating summary for Space $roomId membership: ${roomSummaryEntity.membership}") + val spaceSummaryEntity = SpaceSummaryEntity() + spaceSummaryEntity.spaceId = roomId spaceSummaryEntity.roomSummaryEntity = roomSummaryEntity spaceSummaryEntity.children.clear() spaceSummaryEntity.children.addAll( RoomRelationshipHelper(realm, roomId).getDirectChildrenDescriptions() - .map { RoomSummaryEntity.getOrCreate(realm, it) } + .map { + Timber.v("## Space: Updating summary for room $roomId with info $it") + realm.createObject().apply { + this.roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, it.roomId) + this.order = it.order + this.present = it.present + this.autoJoin = it.autoJoin + }.also { + Timber.v("## Space: Updating summary for room $roomId with children $it") + } + } ) + realm.insertOrUpdate(spaceSummaryEntity) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index 4118d74604..973904cbd5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -42,6 +42,7 @@ internal class DefaultSpaceService @Inject constructor( @SessionDatabase private val monarchy: Monarchy, private val createRoomTask: CreateRoomTask, private val joinRoomTask: JoinRoomTask, + private val joinSpaceTask: JoinSpaceTask, private val markAllRoomsReadTask: MarkAllRoomsReadTask, private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, private val roomIdByAliasTask: GetRoomIdByAliasTask, @@ -77,22 +78,24 @@ internal class DefaultSpaceService @Inject constructor( override suspend fun joinSpace(spaceIdOrAlias: String, reason: String?, viaServers: List, autoJoinChild: List): SpaceService.JoinSpaceResult { try { - joinRoomTask.execute(JoinRoomTask.Params(spaceIdOrAlias, reason, viaServers)) - val childJoinFailures = mutableMapOf() - autoJoinChild.forEach { info -> - // TODO what if the child is it self a subspace with some default children? - try { - joinRoomTask.execute(JoinRoomTask.Params(info.roomIdOrAlias, null, info.viaServers)) - } catch (failure: Throwable) { - // TODO, i could already be a member of this room, handle that as it should not be an error in this context - childJoinFailures[info.roomIdOrAlias] = failure - } - } - return if (childJoinFailures.isEmpty()) { - SpaceService.JoinSpaceResult.Success - } else { - SpaceService.JoinSpaceResult.PartialSuccess(childJoinFailures) - } + joinSpaceTask.execute(JoinSpaceTask.Params(spaceIdOrAlias, reason, viaServers)) + // TODO partial success + return SpaceService.JoinSpaceResult.Success +// val childJoinFailures = mutableMapOf() +// autoJoinChild.forEach { info -> +// // TODO what if the child is it self a subspace with some default children? +// try { +// joinRoomTask.execute(JoinRoomTask.Params(info.roomIdOrAlias, null, info.viaServers)) +// } catch (failure: Throwable) { +// // TODO, i could already be a member of this room, handle that as it should not be an error in this context +// childJoinFailures[info.roomIdOrAlias] = failure +// } +// } +// return if (childJoinFailures.isEmpty()) { +// SpaceService.JoinSpaceResult.Success +// } else { +// SpaceService.JoinSpaceResult.PartialSuccess(childJoinFailures) +// } } catch (throwable: Throwable) { return SpaceService.JoinSpaceResult.Fail(throwable) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt new file mode 100644 index 0000000000..66a695fc18 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space + +import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult +import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity +import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask +import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal interface JoinSpaceTask : Task { + data class Params( + val roomIdOrAlias: String, + val reason: String?, + val viaServers: List = emptyList() + ) +} + +internal class DefaultJoinSpaceTask @Inject constructor( + private val roomAPI: RoomAPI, + private val joinRoomTask: JoinRoomTask, + @SessionDatabase + private val realmConfiguration: RealmConfiguration, + private val spaceSummaryDataSource: SpaceSummaryDataSource, + private val eventBus: EventBus +) : JoinSpaceTask { + + override suspend fun execute(params: JoinSpaceTask.Params) { + Timber.v("## Space: > Joining root space ${params.roomIdOrAlias} ...") + joinRoomTask.execute(JoinRoomTask.Params( + params.roomIdOrAlias, + params.reason, + params.viaServers + )) + Timber.v("## Space: < Joining root space done for ${params.roomIdOrAlias}") + // we want to wait for sync result to check for auto join rooms + + Timber.v("## Space: > Wait for post joined sync ${params.roomIdOrAlias} ...") + try { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(2L)) { realm -> + realm.where(SpaceSummaryEntity::class.java) + .apply { + if (params.roomIdOrAlias.startsWith("!")) { + equalTo(SpaceSummaryEntityFields.SPACE_ID, params.roomIdOrAlias) + } else { + equalTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.CANONICAL_ALIAS, params.roomIdOrAlias) + } + } + .equalTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.MEMBERSHIP_STR, Membership.JOIN.name) + } + } catch (exception: TimeoutCancellationException) { + Timber.w("## Space: > Error created with timeout") + throw CreateRoomFailure.CreatedWithTimeout + } + + Timber.v("## Space: > Sync done ...") + // after that i should have the children (? do i nead to paginate to get state) + val summary = spaceSummaryDataSource.getSpaceSummary(params.roomIdOrAlias) + Timber.v("## Space: Found space summary Name:[${summary?.roomSummary?.name}] children: ${summary?.children?.size}") + summary?.children?.forEach { + val childRoomSummary = it.roomSummary ?: return@forEach + Timber.v("## Space: Processing child :[${childRoomSummary.roomId}] present: ${it.present} autoJoin:${it.autoJoin}") + if (it.present && it.autoJoin) { + // I should try to join as well + if (childRoomSummary.roomType == RoomType.SPACE) { + } else { + try { + Timber.v("## Space: Joining room child ${childRoomSummary.roomId}") + joinRoomTask.execute(JoinRoomTask.Params( + roomIdOrAlias = childRoomSummary.roomId, + reason = "Auto-join parent space", + viaServers = it.viaServers + )) + } catch (failure: Throwable) { + // todo keep track for partial success + Timber.e("## Space: Failed to join room child ${childRoomSummary.roomId}") + } + } + } + } + } +} + +// try { +// awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> +// realm.where(RoomEntity::class.java) +// .equalTo(RoomEntityFields.ROOM_ID, roomId) +// } +// } catch (exception: TimeoutCancellationException) { +// throw CreateRoomFailure.CreatedWithTimeout +// } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt index 63eed2a6c2..a854dd25d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 New Vector Ltd + * 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. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt index 2bb606e921..6859df1d37 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -212,6 +212,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + Timber.v("## Space state event: $eventEntity") eventId = event.eventId root = eventEntity } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 9a1f2e6dfd..e66a123773 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -273,6 +273,7 @@ + () { + + override fun getBinding(): ActivitySimpleBinding = ActivitySimpleBinding.inflate(layoutInflater) + // lateinit var sharedActionViewModel: SpacePreviewSharedActionViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) +// sharedActionViewModel = viewModelProvider.get(SpacePreviewSharedActionViewModel::class.java) +// sharedActionViewModel +// .observe() +// .subscribe { action -> +// when (action) { +// SpacePreviewSharedAction.DismissAction -> finish() +// SpacePreviewSharedAction.ShowModalLoading -> showWaitingView() +// SpacePreviewSharedAction.HideModalLoading -> hideWaitingView() +// is SpacePreviewSharedAction.ShowErrorMessage -> action.error?.let { showSnackbar(it) } +// } +// }.disposeOnDestroy() + + if (isFirstCreation()) { + val simpleName = SpaceDirectoryFragment::class.java.simpleName + val args = intent?.getParcelableExtra(MvRx.KEY_ARG) + if (supportFragmentManager.findFragmentByTag(simpleName) == null) { + supportFragmentManager.commitTransaction { + replace(R.id.simpleFragmentContainer, + SpacePreviewFragment::class.java, + Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) }, + simpleName + ) + } + } + } + } + + companion object { + fun newIntent(context: Context, spaceId: String): Intent { + return Intent(context, SpaceExploreActivity::class.java).apply { + putExtra(MvRx.KEY_ARG, SpaceDirectoryArgs(spaceId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/grouplist/SpaceListFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt similarity index 91% rename from vector/src/main/java/im/vector/app/features/grouplist/SpaceListFragment.kt rename to vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt index 7091c0b86c..e14b920b2c 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/SpaceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt @@ -1,21 +1,20 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2021 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 +package im.vector.app.features.spaces import android.os.Bundle import android.view.LayoutInflater @@ -33,9 +32,6 @@ 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 diff --git a/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryController.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryController.kt rename to vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt index dab2cbceae..29d48b4cd1 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt @@ -1,29 +1,28 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2021 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 +package im.vector.app.features.spaces import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericItemHeader +import im.vector.app.features.grouplist.homeSpaceSummaryItem import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.spaces.SpaceListViewState import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.api.util.toMatrixItem @@ -87,7 +86,6 @@ class SpaceSummaryController @Inject constructor( summaries .filter { it.roomSummary.membership == Membership.JOIN } .forEach { groupSummary -> - val isSelected = groupSummary.spaceId == selected?.spaceId if (groupSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) { homeSpaceSummaryItem { diff --git a/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt similarity index 98% rename from vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryItem.kt rename to vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt index 1a710b764c..525936deab 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/SpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt @@ -15,7 +15,7 @@ * */ -package im.vector.app.features.grouplist +package im.vector.app.features.spaces import android.widget.ImageView import android.widget.TextView 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 index f0d8ae30f7..bdc5c997f6 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt @@ -31,14 +31,12 @@ 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 @@ -117,9 +115,11 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp // PRIVATE METHODS ***************************************************************************** private fun handleSelectSpace(action: SpaceListAction.SelectSpace) = withState { state -> - - if (state.selectedSpace?.roomSummary?.membership == Membership.INVITE) { - _viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(state.selectedSpace.roomSummary.roomId)) + // get uptodate version of the space + val summary = session.spaceService().getSpaceSummaries(roomSummaryQueryParams { roomId = QueryStringValue.Equals(action.spaceSummary.spaceId) }) + .firstOrNull() + if (summary?.roomSummary?.membership == Membership.INVITE) { + _viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(summary.roomSummary.roomId)) // viewModelScope.launch(Dispatchers.IO) { // tryOrNull { session.spaceService().peekSpace(action.spaceSummary.spaceId) }.let { // Timber.d("PEEK RESULT/ $it") @@ -139,7 +139,8 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp val roomSummaryQueryParams = roomSummaryQueryParams() { memberships = listOf(Membership.JOIN, Membership.INVITE) displayName = QueryStringValue.IsNotEmpty - excludeType = listOf(RoomType.MESSAGING, null) + excludeType = listOf(/**RoomType.MESSAGING,$*/ + null) } Observable.combineLatest, List>( session diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt new file mode 100644 index 0000000000..2c46607987 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.explore + +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Success +import im.vector.app.core.epoxy.errorWithRetryItem +import im.vector.app.core.epoxy.loadingItem + +class SpaceDirectoryController : TypedEpoxyController() { + + override fun buildModels(data: SpaceDirectoryState?) { + when (data?.summary) { + is Success -> { +// val directories = roomDirectoryListCreator.computeDirectories(asyncThirdPartyProtocol()) +// +// directories.forEach { +// buildDirectory(it) +// } + } + is Incomplete -> { + loadingItem { + id("loading") + } + } + is Fail -> { + errorWithRetryItem { + id("error") +// text(errorFormatter.toHumanReadable(asyncThirdPartyProtocol.error)) +// listener { callback?.retry() } + } + } + else -> { + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt new file mode 100644 index 0000000000..6b67e20405 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.explore + +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.ViewGroup +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class SpaceDirectoryArgs( + val spaceId: String +) : Parcelable + +class SpaceDirectoryFragment : VectorBaseFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentRoomDirectoryPickerBinding.inflate(layoutInflater, container, false) +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt new file mode 100644 index 0000000000..86baae0875 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.explore + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +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.core.platform.VectorViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.platform.VectorViewModelAction +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.api.session.space.SpaceSummary +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.rx.unwrap + +data class SpaceDirectoryState( + // The current filter + val spaceId: String, + val currentFilter: String = "", + val summary: Async = Uninitialized, + // True if more result are available server side + val hasMore: Boolean = false, + // Set of joined roomId / spaces, + val joinedRoomsIds: Set = emptySet() +) : MvRxState { + constructor(args: SpaceDirectoryArgs) : this(spaceId = args.spaceId) +} + +sealed class SpaceDirectoryViewAction : VectorViewModelAction + +sealed class SpaceDirectoryViewEvents : VectorViewEvents + +class SpaceDirectoryViewModel @AssistedInject constructor( + @Assisted initialState: SpaceDirectoryState, + private val session: Session +) : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: SpaceDirectoryState): SpaceDirectoryViewModel + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: SpaceDirectoryState): SpaceDirectoryViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + init { + val queryParams = roomSummaryQueryParams { + roomId = QueryStringValue.Equals(initialState.spaceId) + } + + viewModelScope.launch(Dispatchers.IO) { + session + .rx() + .liveSpaceSummaries(queryParams) + .observeOn(Schedulers.computation()) + .map { sum -> Optional.from(sum.firstOrNull()) } + .unwrap() + .execute { async -> + copy(summary = async) + } + } + } + + override fun handle(action: VectorViewModelAction) { + TODO("Not yet implemented") + } +} From 7521a0d3ae48b7faa861694ecd5d9dc62ab5c8b7 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 12 Jan 2021 15:37:39 +0100 Subject: [PATCH 05/97] Fix / post rebase issues --- .../android/sdk/internal/session/space/JoinSpaceTask.kt | 4 +--- .../java/im/vector/app/features/spaces/SpaceSummaryItem.kt | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt index 66a695fc18..82dfb55dcc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.space import io.realm.RealmConfiguration import kotlinx.coroutines.TimeoutCancellationException -import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomType @@ -46,8 +45,7 @@ internal class DefaultJoinSpaceTask @Inject constructor( private val joinRoomTask: JoinRoomTask, @SessionDatabase private val realmConfiguration: RealmConfiguration, - private val spaceSummaryDataSource: SpaceSummaryDataSource, - private val eventBus: EventBus + private val spaceSummaryDataSource: SpaceSummaryDataSource ) : JoinSpaceTask { override suspend fun execute(params: JoinSpaceTask.Params) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt index 525936deab..bf3a47461f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt @@ -1,18 +1,17 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2021 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 From b7a89f40552d75f4daaf690cd871b31ecc15b710 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 12 Jan 2021 15:37:48 +0100 Subject: [PATCH 06/97] Fix / Syncs breaking on dendrite --- .../matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt | 2 +- .../sdk/internal/session/sync/model/RoomSyncAccountData.kt | 2 +- .../sdk/internal/session/sync/model/RoomSyncEphemeral.kt | 2 +- .../android/sdk/internal/session/sync/model/RoomSyncState.kt | 2 +- .../android/sdk/internal/session/sync/model/RoomSyncTimeline.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt index 6859df1d37..95fbb2f1b0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -456,7 +456,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) { - for (event in accountData.events) { + accountData.events?.forEach { event -> val eventType = event.getClearType() if (eventType == EventType.TAG) { val content = event.getClearContent().toModel() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt index 1c35d812ee..9e0ccde16b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt @@ -25,5 +25,5 @@ internal data class RoomSyncAccountData( /** * List of account data events (array of Event). */ - @Json(name = "events") val events: List = emptyList() + @Json(name = "events") val events: List? = emptyList() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt index d59dddb3ea..a2e044d947 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt @@ -26,5 +26,5 @@ internal data class RoomSyncEphemeral( /** * List of ephemeral events (array of Event). */ - @Json(name = "events") val events: List = emptyList() + @Json(name = "events") val events: List? = emptyList() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt index 5355b7eef1..c825cbf31c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt @@ -27,5 +27,5 @@ internal data class RoomSyncState( /** * List of state events (array of Event). The resulting state corresponds to the *start* of the timeline. */ - @Json(name = "events") val events: List = emptyList() + @Json(name = "events") val events: List? = emptyList() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt index ddf430099a..ce51958f73 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt @@ -27,7 +27,7 @@ internal data class RoomSyncTimeline( /** * List of events (array of Event). */ - @Json(name = "events") val events: List = emptyList(), + @Json(name = "events") val events: List? = emptyList(), /** * Boolean which tells whether there are more events on the server From 57f17620b554a01ba5a347728a49106d39443b99 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 13 Jan 2021 14:23:02 +0100 Subject: [PATCH 07/97] Fix Dendrite sync response support --- .../session/room/timeline/TokenChunkEventPersistor.kt | 6 +++--- .../sdk/internal/session/room/uploads/GetUploadsTask.kt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index c38dcd00a7..903aef16df 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -124,7 +124,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri direction: PaginationDirection): Result { monarchy .awaitTransaction { realm -> - Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") + Timber.v("Start persisting ${receivedChunk.events?.size} events in $roomId towards $direction") val nextToken: String? val prevToken: String? @@ -149,7 +149,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri } ?: ChunkEntity.create(realm, prevToken, nextToken) - if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) { + if (receivedChunk.events.isNullOrEmpty() && !receivedChunk.hasMore()) { handleReachEnd(realm, roomId, direction, currentChunk) } else { handlePagination(realm, roomId, direction, receivedChunk, currentChunk) @@ -189,7 +189,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri receivedChunk: TokenChunkEvent, currentChunk: ChunkEntity ) { - Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") + Timber.v("Add ${receivedChunk.events?.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") val roomMemberContentsByUser = HashMap() val eventList = receivedChunk.events val stateEvents = receivedChunk.stateEvents diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt index 028c3e9193..74b8372b77 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt @@ -94,7 +94,7 @@ internal class DefaultGetUploadsTask @Inject constructor( nextToken = chunk.end ?: "", hasMore = chunk.hasMore() ) - events = chunk.events + events = chunk.events ?: emptyList() } var uploadEvents = listOf() From 186024b2717b0c388d88e55eb3fc63b0f76999d9 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 15 Jan 2021 09:57:29 +0100 Subject: [PATCH 08/97] MSC 2946 WIP --- .../sdk/api/session/space/SpaceService.kt | 9 ++ .../sdk/internal/session/SessionComponent.kt | 4 +- .../sdk/internal/session/room/RoomModule.kt | 4 - .../session/space/DefaultSpaceService.kt | 62 ++++++- .../internal/session/space/JoinSpaceTask.kt | 2 +- .../session/space/ResolveSpaceInfoTask.kt | 45 ++++++ .../sdk/internal/session/space/SpaceApi.kt | 42 +++++ .../space/SpaceChildSummaryResponse.kt | 96 +++++++++++ .../sdk/internal/session/space/SpaceModule.kt | 48 ++++++ .../session/space/SpaceSummaryParams.kt | 30 ++++ .../internal/session/space/SpacesResponse.kt | 31 ++++ .../session/space/peeking/PeekSpaceTask.kt | 2 +- .../spaces/preview/SpacePreviewController.kt | 153 ++++++++++-------- .../spaces/preview/SpacePreviewFragment.kt | 35 ++-- .../spaces/preview/SpacePreviewState.kt | 16 +- .../spaces/preview/SpacePreviewViewModel.kt | 138 +++++++++++++--- 16 files changed, 600 insertions(+), 117 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpacesResponse.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt index 4043a3f7b4..304fb4d98b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -18,6 +18,8 @@ package org.matrix.android.sdk.api.session.space import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult typealias SpaceSummaryQueryParams = RoomSummaryQueryParams @@ -43,6 +45,11 @@ interface SpaceService { */ suspend fun peekSpace(spaceId: String) : SpacePeekResult + /** + * Get's information of a space by querying the server + */ + suspend fun querySpaceChildren(spaceId: String) : Pair> + /** * Get a live list of space summaries. This list is refreshed as soon as the data changes. * @return the [LiveData] of List[SpaceSummary] @@ -69,4 +76,6 @@ interface SpaceService { reason: String? = null, viaServers: List = emptyList(), autoJoinChild: List) : JoinSpaceResult + + suspend fun rejectInvite(spaceId: String, reason: String?) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index 7e1e3d0f70..541c877b1d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -52,6 +52,7 @@ import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker import org.matrix.android.sdk.internal.session.room.send.SendEventWorker import org.matrix.android.sdk.internal.session.search.SearchModule import org.matrix.android.sdk.internal.session.signout.SignOutModule +import org.matrix.android.sdk.internal.session.space.SpaceModule import org.matrix.android.sdk.internal.session.sync.SyncModule import org.matrix.android.sdk.internal.session.sync.SyncTask import org.matrix.android.sdk.internal.session.sync.SyncTokenStore @@ -91,7 +92,8 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers FederationModule::class, CallModule::class, SearchModule::class, - ThirdPartyModule::class + ThirdPartyModule::class, + SpaceModule::class ] ) @SessionScope 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 8cc7f41d5b..c19c59e6db 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 @@ -90,11 +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.DefaultJoinSpaceTask import org.matrix.android.sdk.internal.session.space.DefaultSpaceService -import org.matrix.android.sdk.internal.session.space.JoinSpaceTask -import org.matrix.android.sdk.internal.session.space.peeking.DefaultPeekSpaceTask -import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask import retrofit2.Retrofit @Module diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index 973904cbd5..42f4ed4742 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -18,12 +18,17 @@ package org.matrix.android.sdk.internal.session.space import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy +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.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.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.api.session.space.model.SpaceChildContent 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 @@ -31,6 +36,7 @@ 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.membership.leaving.LeaveRoomTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult @@ -50,6 +56,8 @@ internal class DefaultSpaceService @Inject constructor( private val roomGetter: RoomGetter, private val spaceSummaryDataSource: SpaceSummaryDataSource, private val peekSpaceTask: PeekSpaceTask, + private val resolveSpaceInfoTask: ResolveSpaceInfoTask, + private val leaveRoomTask: LeaveRoomTask, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, private val taskExecutor: TaskExecutor ) : SpaceService { @@ -76,7 +84,55 @@ internal class DefaultSpaceService @Inject constructor( return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId)) } - override suspend fun joinSpace(spaceIdOrAlias: String, reason: String?, viaServers: List, autoJoinChild: List): SpaceService.JoinSpaceResult { + override suspend fun querySpaceChildren(spaceId: String): Pair> { + return resolveSpaceInfoTask.execute(ResolveSpaceInfoTask.Params.withId(spaceId)).let { response -> + val spaceDesc = response.rooms?.firstOrNull { it.roomId == spaceId } + Pair( + first = RoomSummary( + roomId = spaceDesc?.roomId ?: spaceId, + roomType = spaceDesc?.roomType, + name = spaceDesc?.name ?: "", + displayName = spaceDesc?.name ?: "", + topic = spaceDesc?.topic ?: "", + joinedMembersCount = spaceDesc?.numJoinedMembers, + avatarUrl = spaceDesc?.avatarUrl ?: "", + encryptionEventTs = null, + typingUsers = emptyList(), + isEncrypted = false + ), + second = response.rooms + ?.filter { it.roomId != spaceId } + ?.map { childSummary -> + val childStateEv = response.events + ?.firstOrNull { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD } + ?.content.toModel() + SpaceChildInfo( + roomSummary = RoomSummary( + roomId = childSummary.roomId, + roomType = childSummary.roomType, + name = childSummary.name ?: "", + displayName = childSummary.name ?: "", + topic = childSummary.topic ?: "", + joinedMembersCount = childSummary.numJoinedMembers, + avatarUrl = childSummary.avatarUrl ?: "", + encryptionEventTs = null, + typingUsers = emptyList(), + isEncrypted = false + ), + order = childStateEv?.order, + present = childStateEv?.present ?: false, + autoJoin = childStateEv?.default ?: false, + viaServers = childStateEv?.via ?: emptyList() + ) + } ?: emptyList() + ) + } + } + + override suspend fun joinSpace(spaceIdOrAlias: String, + reason: String?, + viaServers: List, + autoJoinChild: List): SpaceService.JoinSpaceResult { try { joinSpaceTask.execute(JoinSpaceTask.Params(spaceIdOrAlias, reason, viaServers)) // TODO partial success @@ -100,4 +156,8 @@ internal class DefaultSpaceService @Inject constructor( return SpaceService.JoinSpaceResult.Fail(throwable) } } + + override suspend fun rejectInvite(spaceId: String, reason: String?) { + leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason)) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt index 82dfb55dcc..1878d2c0f9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * 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. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt new file mode 100644 index 0000000000..1eacdce8df --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.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.internal.session.space + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface ResolveSpaceInfoTask : Task { + data class Params( + val spaceId: String, + val maxRoomPerSpace: Int, + val limit: Int, + val batchToken: String? + ) { + companion object { + fun withId(spaceId: String) = Params(spaceId, 10, 20, null) + } + } +} + +internal class DefaultResolveSpaceInfoTask @Inject constructor( + private val spaceApi: SpaceApi +) : ResolveSpaceInfoTask { + override suspend fun execute(params: ResolveSpaceInfoTask.Params): SpacesResponse { + val body = SpaceSummaryParams(maxRoomPerSpace = params.maxRoomPerSpace, limit = params.limit, batch = params.batchToken ?: "") + return executeRequest(null) { + apiCall = spaceApi.getSpaces(params.spaceId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt new file mode 100644 index 0000000000..5919a90b99 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt @@ -0,0 +1,42 @@ +/* + * 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.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Path + +internal interface SpaceApi { + + /** + * + * POST /_matrix/client/r0/rooms/{roomID}/spaces + * { + * "max_rooms_per_space": 5, // The maximum number of rooms/subspaces to return for a given space, if negative unbounded. default: -1. + * "limit": 100, // The maximum number of rooms/subspaces to return, server can override this, default: 100. + * "batch": "opaque_string" // A token to use if this is a subsequent HTTP hit, default: "". + * } + * + * MSC 2946 https://github.com/matrix-org/matrix-doc/blob/kegan/spaces-summary/proposals/2946-spaces-summary.md + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/spaces") + fun getSpaces(@Path("roomId") spaceId: String, + @Body params: SpaceSummaryParams + ): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt new file mode 100644 index 0000000000..5021ff638f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt @@ -0,0 +1,96 @@ +/* + * 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 com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SpaceChildSummaryResponse( + /** + * The total number of state events which point to or from this room (inbound/outbound edges). + * This includes all m.space.child events in the room, in addition to m.room.parent events which point to this room as a parent. + */ + @Json(name = "num_refs") val numRefs: Int? = null, + + /** + * The room type, which is m.space for subspaces. + * It can be omitted if there is no room type in which case it should be interpreted as a normal room. + */ + @Json(name = "room_type") val roomType: String? = null, + + /** + * Aliases of the room. May be empty. + */ + @Json(name = "aliases") + val aliases: List? = null, + + /** + * The canonical alias of the room, if any. + */ + @Json(name = "canonical_alias") + val canonicalAlias: String? = null, + + /** + * The name of the room, if any. + */ + @Json(name = "name") + val name: String? = null, + + /** + * Required. The number of members joined to the room. + */ + @Json(name = "num_joined_members") + val numJoinedMembers: Int = 0, + + /** + * Required. The ID of the room. + */ + @Json(name = "room_id") + val roomId: String, + + /** + * The topic of the room, if any. + */ + @Json(name = "topic") + val topic: String? = null, + + /** + * Required. Whether the room may be viewed by guest users without joining. + */ + @Json(name = "world_readable") + val worldReadable: Boolean = false, + + /** + * Required. Whether guest users may join the room and participate in it. If they can, + * they will be subject to ordinary power level rules like any other user. + */ + @Json(name = "guest_can_join") + val guestCanJoin: Boolean = false, + + /** + * The URL for the room's avatar, if one is set. + */ + @Json(name = "avatar_url") + val avatarUrl: String? = null, + + /** + * Undocumented item + */ + @Json(name = "m.federate") + val isFederated: Boolean = false +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt new file mode 100644 index 0000000000..ba15f2e981 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.space.peeking.DefaultPeekSpaceTask +import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask +import retrofit2.Retrofit + +@Module +internal abstract class SpaceModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesSpacesAPI(retrofit: Retrofit): SpaceApi { + return retrofit.create(SpaceApi::class.java) + } + } + + @Binds + abstract fun bindResolveSpaceTask(task: DefaultResolveSpaceInfoTask): ResolveSpaceInfoTask + + @Binds + abstract fun bindPeekSpaceTask(task: DefaultPeekSpaceTask): PeekSpaceTask + + @Binds + abstract fun bindJoinSpaceTask(task: DefaultJoinSpaceTask): JoinSpaceTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt new file mode 100644 index 0000000000..a3c6b3cc84 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt @@ -0,0 +1,30 @@ +/* + * 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 com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SpaceSummaryParams( + /** The maximum number of rooms/subspaces to return for a given space, if negative unbounded. default: -1*/ + @Json(name = "max_rooms_per_space") val maxRoomPerSpace: Int = 100, + /** The maximum number of rooms/subspaces to return, server can override this, default: 100 */ + @Json(name = "limit") val limit: Int = 100, + /** A token to use if this is a subsequent HTTP hit, default: "".*/ + @Json(name = "batch") val batch: String = "" +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpacesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpacesResponse.kt new file mode 100644 index 0000000000..20d63c8814 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpacesResponse.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.internal.session.space + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class SpacesResponse( + /** Its presence indicates that there are more results to return. */ + @Json(name = "next_batch") val nextBatch: String? = null, + /** Rooms information like name/avatar/type ... */ + @Json(name = "rooms") val rooms: List? = null, + /** These are the edges of the graph. The objects in the array are complete (or stripped?) m.room.parent or m.space.child events. */ + @Json(name = "events") val events: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt index a2be75a232..1214befebd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 New Vector Ltd + * 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. diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt index 651411b2fe..eee8d1241f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt @@ -17,17 +17,14 @@ package im.vector.app.features.spaces.preview import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success import im.vector.app.R +import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.resources.StringProvider -import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericItemHeader import im.vector.app.core.utils.TextUtils import im.vector.app.features.home.AvatarRenderer -import org.matrix.android.sdk.api.session.room.peeking.PeekResult -import org.matrix.android.sdk.internal.session.space.peeking.ISpaceChild -import org.matrix.android.sdk.internal.session.space.peeking.SpaceChildPeekResult -import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult -import org.matrix.android.sdk.internal.session.space.peeking.SpaceSubChildPeekResult import javax.inject.Inject class SpacePreviewController @Inject constructor( @@ -40,78 +37,100 @@ class SpacePreviewController @Inject constructor( var interactionListener: InteractionListener? = null override fun buildModels(data: SpacePreviewState?) { - val result: SpacePeekResult = data?.peekResult?.invoke() ?: return + val result = data?.childInfoList?.invoke() ?: return - when (result) { - is SpacePeekResult.SpacePeekError -> { - genericFooterItem { - id("failed") - // TODO - text("Failed to resolve") - } + val memberCount = data.spaceInfo.invoke()?.memberCount ?: 0 + + spaceTopSummaryItem { + id("info") + formattedMemberCount(stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)) + topic(data.spaceInfo.invoke()?.topic ?: data.topic ?: "") + } + + if (result.isNotEmpty()) { + genericItemHeader { + id("header_rooms") + text(stringProvider.getString(R.string.rooms)) } - is SpacePeekResult.Success -> { - // add summary info - val memberCount = result.summary.roomPeekResult.numJoinedMembers ?: 0 - spaceTopSummaryItem { - id("info") - formattedMemberCount(stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)) - topic(result.summary.roomPeekResult.topic ?: "") - } - - genericItemHeader { - id("header_rooms") - text(stringProvider.getString(R.string.rooms)) - } - - buildChildren(result.summary.children, 0) - } + buildChildren(result, 0) } } - private fun buildChildren(children: List, depth: Int) { + private fun buildChildren(children: List, depth: Int) { children.forEach { child -> - when (child) { - is SpaceSubChildPeekResult -> { - when (val roomPeekResult = child.roomPeekResult) { - is PeekResult.Success -> { - subSpaceItem { - id(roomPeekResult.roomId) - roomId(roomPeekResult.roomId) - title(roomPeekResult.name) - depth(depth) - avatarUrl(roomPeekResult.avatarUrl) - avatarRenderer(avatarRenderer) - } - buildChildren(child.children, depth + 1) - } - else -> { - // ?? TODO - } + + if (child.isSubSpace == true) { + subSpaceItem { + id(child.roomId) + roomId(child.roomId) + title(child.name) + depth(depth) + avatarUrl(child.avatarUrl) + avatarRenderer(avatarRenderer) + } + when (child.children) { + is Loading -> { + loadingItem { id("loading_children_${child.roomId}") } + } + is Success -> { + buildChildren(child.children.invoke(), depth + 1) + } + else -> { } } - is SpaceChildPeekResult -> { - // We have to check if the peek result was success - when (val roomPeekResult = child.roomPeekResult) { - is PeekResult.Success -> { - roomChildItem { - id(child.id) - depth(depth) - roomId(roomPeekResult.roomId) - title(roomPeekResult.name ?: "") - topic(roomPeekResult.topic ?: "") - avatarUrl(roomPeekResult.avatarUrl) - memberCount(TextUtils.formatCountToShortDecimal(roomPeekResult.numJoinedMembers ?: 0)) - avatarRenderer(avatarRenderer) - } - } - else -> { - // What to do here? - } - } + } else { + roomChildItem { + id(child.roomId) + depth(depth) + roomId(child.roomId) + title(child.name ?: "") + topic(child.topic ?: "") + avatarUrl(child.avatarUrl) + memberCount(TextUtils.formatCountToShortDecimal(child.memberCount ?: 0)) + avatarRenderer(avatarRenderer) } } +// when (child) { +// is SpaceSubChildPeekResult -> { +// when (val roomPeekResult = child.roomPeekResult) { +// is PeekResult.Success -> { +// subSpaceItem { +// id(roomPeekResult.roomId) +// roomId(roomPeekResult.roomId) +// title(roomPeekResult.name) +// depth(depth) +// avatarUrl(roomPeekResult.avatarUrl) +// avatarRenderer(avatarRenderer) +// } +// buildChildren(child.children, depth + 1) +// } +// else -> { +// // ?? TODO +// } +// } +// } +// is SpaceChildPeekResult -> { +// // We have to check if the peek result was success +// when (val roomPeekResult = child.roomPeekResult) { +// is PeekResult.Success -> { +// roomChildItem { +// id(child.id) +// depth(depth) +// roomId(roomPeekResult.roomId) +// title(roomPeekResult.name ?: "") +// topic(roomPeekResult.topic ?: "") +// avatarUrl(roomPeekResult.avatarUrl) +// memberCount(TextUtils.formatCountToShortDecimal(roomPeekResult.numJoinedMembers ?: 0)) +// avatarRenderer(avatarRenderer) +// } +// } +// else -> { +// // What to do here? +// } +// } +// } +// } } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt index fcf961f23a..563b4f39e0 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt @@ -40,7 +40,6 @@ import im.vector.app.features.spaces.SpacePreviewSharedActionViewModel import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.util.MatrixItem -import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -99,7 +98,7 @@ class SpacePreviewFragment @Inject constructor( } override fun invalidate() = withState(viewModel) { - when (it.peekResult) { + when (it.spaceInfo) { is Uninitialized, is Loading -> { views.spacePreviewPeekingProgress.isVisible = true @@ -141,21 +140,23 @@ class SpacePreviewFragment @Inject constructor( } private fun updateToolbar(spacePreviewState: SpacePreviewState) { - when (val preview = spacePreviewState.peekResult.invoke()) { - is SpacePeekResult.Success -> { - val roomPeekResult = preview.summary.roomPeekResult - val mxItem = MatrixItem.RoomItem(roomPeekResult.roomId, roomPeekResult.name, roomPeekResult.avatarUrl) - avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar) - views.roomPreviewNoPreviewToolbarTitle.text = roomPeekResult.name - } - is SpacePeekResult.SpacePeekError, - null -> { - // what to do here? - val mxItem = MatrixItem.RoomItem(spacePreviewState.idOrAlias, spacePreviewState.name, spacePreviewState.avatarUrl) - avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar) - views.roomPreviewNoPreviewToolbarTitle.text = spacePreviewState.name - } - } +// when (val preview = spacePreviewState.peekResult.invoke()) { +// is SpacePeekResult.Success -> { +// val roomPeekResult = preview.summary.roomPeekResult + val spaceName = spacePreviewState.spaceInfo.invoke()?.name ?: spacePreviewState.name ?: "" + val spaceAvatarUrl = spacePreviewState.spaceInfo.invoke()?.avatarUrl ?: spacePreviewState.avatarUrl + val mxItem = MatrixItem.RoomItem(spacePreviewState.idOrAlias, spaceName, spaceAvatarUrl) + avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar) + views.roomPreviewNoPreviewToolbarTitle.text = spaceName +// } +// is SpacePeekResult.SpacePeekError, +// null -> { +// // what to do here? +// val mxItem = MatrixItem.RoomItem(spacePreviewState.idOrAlias, spacePreviewState.name, spacePreviewState.avatarUrl) +// avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar) +// views.roomPreviewNoPreviewToolbarTitle.text = spacePreviewState.name +// } +// } } override fun onStart() { diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt index 41d94e8c9d..cf64672046 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt @@ -19,13 +19,25 @@ package im.vector.app.features.spaces.preview import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized -import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult data class SpacePreviewState( val idOrAlias: String, val name: String? = null, + val topic: String? = null, val avatarUrl: String? = null, - val peekResult: Async = Uninitialized + val spaceInfo: Async = Uninitialized, + val childInfoList: Async> = Uninitialized ) : MvRxState { constructor(args: SpacePreviewArgs) : this(idOrAlias = args.idOrAlias) } + +data class ChildInfo( + val roomId: String, + val avatarUrl: String?, + val name: String?, + val topic: String?, + val memberCount: Int?, + val isSubSpace: Boolean?, + val viaServers: List?, + val children: Async> +) diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt index 6986db18aa..d11fe1502b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt @@ -23,6 +23,7 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -30,9 +31,12 @@ import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult +import org.matrix.android.sdk.internal.session.space.peeking.SpaceSubChildPeekResult +import timber.log.Timber class SpacePreviewViewModel @AssistedInject constructor( @Assisted private val initialState: SpacePreviewState, @@ -73,31 +77,29 @@ class SpacePreviewViewModel @AssistedInject constructor( } } - private fun handleDismissInvite() { - TODO("Not yet implemented") + private fun handleDismissInvite() = withState { state -> + // Here we need to join the space himself as well as the default rooms in that space + // TODO modal loading + viewModelScope.launch(Dispatchers.IO) { + try { + session.spaceService().rejectInvite(initialState.idOrAlias, null) + } catch (failure: Throwable) { + Timber.e(failure, "## Space: Failed to reject invite") + } + } } private fun handleAcceptInvite() = withState { state -> // Here we need to join the space himself as well as the default rooms in that space - val spaceInfo = state.peekResult.invoke() as? SpacePeekResult.Success - // TODO if we have no summary, we cannot find auto join rooms... // So maybe we should trigger a retry on summary after the join? - val spaceVia = (spaceInfo?.summary?.roomPeekResult as? PeekResult.Success)?.viaServers ?: emptyList() - val autoJoinChildren = spaceInfo?.summary?.children - ?.filter { it.default == true } - ?.map { - SpaceService.ChildAutoJoinInfo( - it.id, - // via servers - (it.roomPeekResult as? PeekResult.Success)?.viaServers ?: emptyList() - ) - } ?: emptyList() + val spaceInfo = state.spaceInfo.invoke() + val spaceVia = spaceInfo?.viaServers ?: emptyList() // trigger modal loading _viewEvents.post(SpacePreviewViewEvents.StartJoining) viewModelScope.launch(Dispatchers.IO) { - val joinResult = session.spaceService().joinSpace(spaceInfo?.summary?.idOrAlias ?: initialState.idOrAlias, null, spaceVia, autoJoinChildren) + val joinResult = session.spaceService().joinSpace(initialState.idOrAlias, null, spaceVia, emptyList()) when (joinResult) { SpaceService.JoinSpaceResult.Success, is SpaceService.JoinSpaceResult.PartialSuccess -> { @@ -116,20 +118,110 @@ class SpacePreviewViewModel @AssistedInject constructor( initialized = true // peek for the room setState { - copy(peekResult = Loading()) + copy( + spaceInfo = Loading(), + childInfoList = Loading() + ) } viewModelScope.launch(Dispatchers.IO) { try { - val result = session.spaceService().peekSpace(initialState.idOrAlias) - setState { - copy(peekResult = Success(result)) - } + resolveSpaceInfo() } catch (failure: Throwable) { - setState { - copy(peekResult = Fail(failure)) - } + Timber.e(failure, "## Space: Failed to resolve space info. Fallback to picking") + fallBackResolve() } } } } + + private suspend fun resolveSpaceInfo() { + val resolveResult = session.spaceService().querySpaceChildren(initialState.idOrAlias) + setState { + copy( + spaceInfo = Success( + resolveResult.first.let { + ChildInfo( + roomId = it.roomId, + avatarUrl = it.avatarUrl, + name = it.name, + topic = it.topic, + memberCount = it.joinedMembersCount, + isSubSpace = it.roomType == RoomType.SPACE, + children = Uninitialized, + viaServers = null + ) + } + ), + childInfoList = Success( + resolveResult.second.map { + ChildInfo( + roomId = it.roomSummary?.roomId ?: "", + avatarUrl = it.roomSummary?.avatarUrl, + name = it.roomSummary?.name, + topic = it.roomSummary?.topic, + memberCount = it.roomSummary?.joinedMembersCount, + isSubSpace = it.roomSummary?.roomType == RoomType.SPACE, + children = Uninitialized, + viaServers = null + ) + } + ) + ) + } + } + + private suspend fun fallBackResolve() { + try { + val resolveResult: SpacePeekResult = session.spaceService().peekSpace(initialState.idOrAlias) + val spaceInfo = (resolveResult as? SpacePeekResult.Success)?.summary?.roomPeekResult + setState { + copy( + spaceInfo = Success( + ChildInfo( + roomId = spaceInfo?.roomId ?: initialState.idOrAlias, + avatarUrl = spaceInfo?.avatarUrl, + name = spaceInfo?.name, + topic = spaceInfo?.topic, + memberCount = spaceInfo?.numJoinedMembers, + isSubSpace = true, + children = Uninitialized, + viaServers = spaceInfo?.viaServers + + ) + ), + childInfoList = resolveResult.let { + when (it) { + is SpacePeekResult.Success -> { + (resolveResult as SpacePeekResult.Success).summary.children.mapNotNull { spaceChild -> + val roomPeekResult = spaceChild.roomPeekResult + if (roomPeekResult is PeekResult.Success) { + ChildInfo( + roomId = spaceChild.id, + avatarUrl = roomPeekResult.avatarUrl, + name = roomPeekResult.name, + topic = roomPeekResult.topic, + memberCount = roomPeekResult.numJoinedMembers, + isSubSpace = spaceChild is SpaceSubChildPeekResult, + children = Uninitialized, + viaServers = roomPeekResult.viaServers + + ) + } else { + null + } + } + Success(emptyList()) + } + else -> { + Fail(Exception("Failed to get info")) + } + } + }) + } + } catch (failure: Throwable) { + setState { + copy(spaceInfo = Fail(failure), childInfoList = Fail(failure)) + } + } + } } From a8d7c25244f688965147bfe7f455d9b20e2da2ae Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 10 Feb 2021 16:36:05 +0100 Subject: [PATCH 09/97] rebase fix --- .../java/im/vector/app/features/home/AvatarRenderer.kt | 2 ++ .../java/im/vector/app/features/home/HomeDetailFragment.kt | 1 - .../im/vector/app/features/spaces/SpacesListViewModel.kt | 7 ++++--- .../app/features/spaces/explore/SpaceDirectoryViewModel.kt | 7 ++++--- .../app/features/spaces/preview/SpacePreviewViewModel.kt | 7 ++++--- 5 files changed, 14 insertions(+), 10 deletions(-) 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 1765372548..f9a68ba465 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 @@ -26,7 +26,9 @@ import androidx.core.graphics.drawable.toBitmap import com.amulyakhare.textdrawable.TextDrawable import com.bumptech.glide.load.MultiTransformation import com.bumptech.glide.load.Transformation +import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.resource.bitmap.CircleCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.DrawableImageViewTarget import com.bumptech.glide.request.target.Target 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 de24be1a7d..6bc3f27fd4 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 @@ -25,7 +25,6 @@ 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 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 index bdc5c997f6..cbfa760f56 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt @@ -23,8 +23,9 @@ 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 dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewModel @@ -66,7 +67,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp private val stringProvider: StringProvider ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: SpaceListViewState): SpacesListViewModel } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt index 86baae0875..a5d1e16101 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt @@ -24,8 +24,9 @@ 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 dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction @@ -62,7 +63,7 @@ class SpaceDirectoryViewModel @AssistedInject constructor( private val session: Session ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: SpaceDirectoryState): SpaceDirectoryViewModel } diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt index d11fe1502b..f5dd8b76ec 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt @@ -25,8 +25,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -54,7 +55,7 @@ class SpacePreviewViewModel @AssistedInject constructor( } } - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: SpacePreviewState): SpacePreviewViewModel } From c8916ee83c8b2bdeaafe6dfc94fb85199581c94d Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 11 Feb 2021 13:12:02 +0100 Subject: [PATCH 10/97] Udpate since msc 1772 --- .../sdk/session/space/SpaceCreationTest.kt | 188 ++++++++++++++++++ .../sdk/api/session/events/model/EventType.kt | 7 +- .../api/session/room/alias/RoomAliasError.kt | 2 +- .../room/model/PowerLevelsContentOverride.kt | 67 +++++++ .../api/session/room/model/SpaceChildInfo.kt | 1 - .../room/model/create/CreateRoomParams.kt | 6 +- .../room/powerlevels/PowerLevelsHelper.kt | 5 +- .../api/session/space/CreateSpaceParams.kt | 8 + .../android/sdk/api/session/space/Space.kt | 4 +- .../sdk/api/session/space/SpaceService.kt | 18 +- .../session/space/model/SpaceChildContent.kt | 31 ++- .../session/space/model/SpaceParentContent.kt | 54 +++++ .../database/RealmSessionStoreMigration.kt | 2 - .../database/mapper/SpaceSummaryMapper.kt | 1 - .../database/model/SpaceChildInfoEntity.kt | 4 +- .../alias/RoomAliasAvailabilityChecker.kt | 6 +- .../session/room/create/CreateRoomBody.kt | 4 +- .../relationship/RoomRelationshipHelper.kt | 23 ++- .../room/state/SafePowerLevelContent.kt | 2 +- .../room/summary/RoomSummaryUpdater.kt | 2 +- .../internal/session/space/DefaultSpace.kt | 29 ++- .../session/space/DefaultSpaceService.kt | 40 ++-- .../internal/session/space/JoinSpaceTask.kt | 49 +++-- .../sdk/internal/session/space/SpaceModule.kt | 2 +- .../session/space/peeking/PeekSpaceTask.kt | 10 +- .../im/vector/app/features/command/Command.kt | 4 +- .../app/features/command/CommandParser.kt | 12 ++ .../app/features/command/ParsedCommand.kt | 2 + .../home/room/detail/RoomDetailViewModel.kt | 40 +++- .../createroom/CreateRoomViewModel.kt | 13 +- .../createroom/RoomAliasErrorFormatter.kt | 2 +- .../spaces/preview/SpacePreviewViewModel.kt | 2 +- vector/src/main/res/values/strings.xml | 3 + 33 files changed, 544 insertions(+), 99 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContentOverride.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt new file mode 100644 index 0000000000..90470233a1 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.space + +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.query.QueryStringValue +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.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.api.session.space.SpaceService +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.internal.util.awaitCallback +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class SpaceCreationTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + + @Test + fun createSimplePublicSpace() { + val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true)) + val roomName = "My Space" + val topic = "A public space for test" + val spaceId: String + runBlocking { + spaceId = session.spaceService().createSpace(roomName, topic, null, true) + // wait a bit to let the summry update it self :/ + delay(400) + } + + val syncedSpace = session.spaceService().getSpace(spaceId) + assertEquals(roomName, syncedSpace?.asRoom()?.roomSummary()?.name, "Room name should be set") + assertEquals(topic, syncedSpace?.asRoom()?.roomSummary()?.topic, "Room topic should be set") + // assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set") + + assertNotNull(syncedSpace, "Space should be found by Id") + val creationEvent = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_CREATE) + val createContent = creationEvent?.content.toModel() + assertEquals(RoomType.SPACE, createContent?.type, "Room type should be space") + + var powerLevelsContent: PowerLevelsContent? = null + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val toModel = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)?.content.toModel() + powerLevelsContent = toModel + toModel != null + } + } + assertEquals(100, powerLevelsContent?.eventsDefault, "Space-rooms should be created with a power level for events_default of 100") + + commonTestHelper.signOutAndClose(session) + } + + @Test + fun testJoinSimplePublicSpace() { + val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true)) + val bobSession = commonTestHelper.createAccount("alice", SessionTestParams(true)) + + val roomName = "My Space" + val topic = "A public space for test" + val spaceId: String + runBlocking { + spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true) + // wait a bit to let the summry update it self :/ + delay(400) + } + + // Try to join from bob, it's a public space no need to invite + + val joinResult: SpaceService.JoinSpaceResult + runBlocking { + joinResult = bobSession.spaceService().joinSpace(spaceId) + } + + assertEquals(SpaceService.JoinSpaceResult.Success, joinResult) + + val spaceBobPov = bobSession.spaceService().getSpace(spaceId) + assertEquals(roomName, spaceBobPov?.asRoom()?.roomSummary()?.name, "Room name should be set") + assertEquals(topic, spaceBobPov?.asRoom()?.roomSummary()?.topic, "Room topic should be set") + + commonTestHelper.signOutAndClose(aliceSession) + commonTestHelper.signOutAndClose(bobSession) + } + + @Test + fun testSimplePublicSpaceWithChildren() { + val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true)) + val bobSession = commonTestHelper.createAccount("alice", SessionTestParams(true)) + + val roomName = "My Space" + val topic = "A public space for test" + val spaceId: String + val firstChild: String + val secondChild: String + + spaceId = runBlocking { aliceSession.spaceService().createSpace(roomName, topic, null, true) } + val syncedSpace = aliceSession.spaceService().getSpace(spaceId) + + // create a room + firstChild = runBlocking { + awaitCallback { + aliceSession.createRoom(CreateRoomParams().apply { + this.name = "FirstRoom" + this.topic = "Description of first room" + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + }, it) + } + } + + runBlocking { + syncedSpace?.addChildren(firstChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", true) + } + + secondChild = runBlocking { + awaitCallback { + aliceSession.createRoom(CreateRoomParams().apply { + this.name = "SecondRoom" + this.topic = "Description of second room" + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + }, it) + } + } + + runBlocking { + syncedSpace?.addChildren(secondChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", false) + } + + // Try to join from bob, it's a public space no need to invite + + val joinResult = runBlocking { + bobSession.spaceService().joinSpace(spaceId) + } + + assertEquals(SpaceService.JoinSpaceResult.Success, joinResult) + + val spaceBobPov = bobSession.spaceService().getSpace(spaceId) + assertEquals(roomName, spaceBobPov?.asRoom()?.roomSummary()?.name, "Room name should be set") + assertEquals(topic, spaceBobPov?.asRoom()?.roomSummary()?.topic, "Room topic should be set") + + // check if bob has joined automatically the first room + + val bobMembershipFirstRoom = bobSession.getRoom(firstChild)?.roomSummary()?.membership + assertEquals(Membership.JOIN, bobMembershipFirstRoom, "Bob should have joined this room") + RoomSummaryQueryParams.Builder() + + val spaceSummaryBobPov = bobSession.spaceService().getSpaceSummaries(roomSummaryQueryParams { + this.roomId = QueryStringValue.Equals(spaceId) + this.memberships = listOf(Membership.JOIN) + }).firstOrNull() + + assertEquals(2, spaceSummaryBobPov?.children?.size ?: -1, "Unexpected number of children") + + commonTestHelper.signOutAndClose(aliceSession) + commonTestHelper.signOutAndClose(bobSession) + } +} 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 99934d3d95..b4a58e5ee6 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,9 +51,13 @@ 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" + + // const val STATE_SPACE_CHILD = "m.space.child" const val STATE_SPACE_CHILD = "org.matrix.msc1772.space.child" + // const val STATE_SPACE_PARENT = "m.space.parent" + const val STATE_SPACE_PARENT = "org.matrix.msc1772.space.parent" + /** * Note that this Event has been deprecated, see * - https://matrix.org/docs/spec/client_server/r0.6.1#historical-events @@ -76,6 +80,7 @@ object EventType { const val CALL_NEGOTIATE = "m.call.negotiate" const val CALL_REJECT = "m.call.reject" const val CALL_HANGUP = "m.call.hangup" + // This type is not processed by the client, just sent to the server const val CALL_REPLACES = "m.call.replaces" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt index d2cb7c58a9..1102eda11c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.api.session.room.alias sealed class RoomAliasError : Throwable() { - object AliasEmpty : RoomAliasError() + object AliasIsBlank : RoomAliasError() object AliasNotAvailable : RoomAliasError() object AliasInvalid : RoomAliasError() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContentOverride.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContentOverride.kt new file mode 100644 index 0000000000..577228cf44 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContentOverride.kt @@ -0,0 +1,67 @@ +/* + * 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 + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content. + */ +@JsonClass(generateAdapter = true) +data class PowerLevelsContentOverride( + /** + * The level required to ban a user. Defaults to 50 if unspecified. + */ + @Json(name = "ban") val ban: Int? = null, + /** + * The level required to kick a user. Defaults to 50 if unspecified. + */ + @Json(name = "kick") val kick: Int? = null, + /** + * The level required to invite a user. Defaults to 50 if unspecified. + */ + @Json(name = "invite") val invite: Int? = null, + /** + * The level required to redact an event. Defaults to 50 if unspecified. + */ + @Json(name = "redact") val redact: Int? = null, + /** + * The default level required to send message events. Can be overridden by the events key. Defaults to 0 if unspecified. + */ + @Json(name = "events_default") val eventsDefault: Int? = null, + /** + * The level required to send specific event types. This is a mapping from event type to power level required. + */ + @Json(name = "events") val events: Map? = null, + /** + * The default power level for every user in the room, unless their user_id is mentioned in the users key. Defaults to 0 if unspecified. + */ + @Json(name = "users_default") val usersDefault: Int? = null, + /** + * The power levels for specific users. This is a mapping from user_id to power level for that user. + */ + @Json(name = "users") val users: Map? = null, + /** + * The default level required to send state events. Can be overridden by the events key. Defaults to 50 if unspecified. + */ + @Json(name = "state_default") val stateDefault: Int? = null, + /** + * The power level requirements for specific notification types. This is a mapping from key to power level for that notifications key. + */ + @Json(name = "notifications") val notifications: Map? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt index 04f3310a77..38d9f1e74e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.api.session.room.model data class SpaceChildInfo( val roomSummary: IRoomSummary?, - val present: Boolean, val order: String?, val autoJoin: Boolean, val viaServers: List 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 880854da58..d4b1a8647a 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 @@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.create import android.net.Uri 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.PowerLevelsContentOverride 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.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM @@ -125,7 +125,7 @@ open class CreateRoomParams { /** * The power level content to override in the default power level event */ - var powerLevelContentOverride: PowerLevelsContent? = null + var powerLevelContentOverride: PowerLevelsContentOverride? = null /** * Mark as a direct message room. @@ -149,6 +149,6 @@ open class CreateRoomParams { companion object { private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate" - private const val CREATION_CONTENT_KEY_ROOM_TYPE = "type" + private const val CREATION_CONTENT_KEY_ROOM_TYPE = "org.matrix.msc1772.type" } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt index 4f1253c6df..7918376ca9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt @@ -31,9 +31,8 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { * @return the power level */ fun getUserPowerLevelValue(userId: String): Int { - return powerLevelsContent.users.getOrElse(userId) { - powerLevelsContent.usersDefault - } + return powerLevelsContent.users?.get(userId) + ?: powerLevelsContent.usersDefault } /** 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 index 0caa7af14c..9776baf567 100644 --- 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 @@ -16,12 +16,20 @@ package org.matrix.android.sdk.api.session.space +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContentOverride 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 { + // Space-rooms are distinguished from regular messaging rooms by the m.room.type of m.space roomType = RoomType.SPACE + + // Space-rooms should be created with a power level for events_default of 100, + // to prevent the rooms accidentally/maliciously clogging up with messages from random members of the space. + powerLevelContentOverride = PowerLevelsContentOverride( + eventsDefault = 100 + ) } } 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 index 88ac00cc55..616393d00b 100644 --- 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 @@ -22,7 +22,9 @@ interface Space { fun asRoom() : Room - suspend fun addRoom(roomId: String) + suspend fun addChildren(roomId: String, viaServers: List, order: String?, autoJoin: Boolean = false) + + suspend fun removeRoom(roomId: String) // fun getChildren() : List } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt index 304fb4d98b..bef3267086 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.api.session.space +import android.net.Uri import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -31,6 +32,11 @@ interface SpaceService { */ suspend fun createSpace(params: CreateSpaceParams): String + /** + * Just a shortcut for space creation for ease of use + */ + suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean): String + /** * Get a space from a roomId * @param roomId the roomId to look for. @@ -43,12 +49,12 @@ interface SpaceService { * Use this call get preview of children of this space, particularly useful to get a * preview of rooms that you did not join yet. */ - suspend fun peekSpace(spaceId: String) : SpacePeekResult + suspend fun peekSpace(spaceId: String): SpacePeekResult /** * Get's information of a space by querying the server */ - suspend fun querySpaceChildren(spaceId: String) : Pair> + suspend fun querySpaceChildren(spaceId: String): Pair> /** * Get a live list of space summaries. This list is refreshed as soon as the data changes. @@ -64,8 +70,9 @@ interface SpaceService { ) sealed class JoinSpaceResult { - object Success: JoinSpaceResult() - data class Fail(val error: Throwable?): JoinSpaceResult() + object Success : JoinSpaceResult() + data class Fail(val error: Throwable) : JoinSpaceResult() + /** Success fully joined the space, but failed to join all or some of it's rooms */ data class PartialSuccess(val failedRooms: Map) : JoinSpaceResult() @@ -74,8 +81,7 @@ interface SpaceService { suspend fun joinSpace(spaceIdOrAlias: String, reason: String? = null, - viaServers: List = emptyList(), - autoJoinChild: List) : JoinSpaceResult + viaServers: List = emptyList()): JoinSpaceResult suspend fun rejectInvite(spaceId: String, reason: String?) } 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 index e31ff5af5c..f7abf7e618 100644 --- 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 @@ -31,13 +31,9 @@ import com.squareup.moshi.JsonClass data class SpaceChildContent( /** * Key which gives a list of candidate servers that can be used to join the room + * Children where via is not present are ignored. */ @Json(name = "via") val via: List? = null, - /** - * present: true key is included to distinguish from a deleted state event - * Children where present is not present or is not set to true are ignored. - */ - @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. @@ -46,8 +42,25 @@ data class SpaceChildContent( */ @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. + * The auto_join flag on a child listing allows a space admin to list the sub-spaces and rooms in that space which should + * be automatically joined by members of that space. + * (This is not a force-join, which are descoped for a future MSC; the user can subsequently part these room if they desire.) */ - @Json(name = "default") val default: Boolean? = false -) + @Json(name = "auto_join") val autoJoin: Boolean? = false +) { + /** + * 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.) + */ + fun validOrder(): String? { + order?.let { + if (order.length > 50) return null + if (!ORDER_VALID_CHAR_REGEX.matches(it)) return null + } + return order + } + + companion object { + private val ORDER_VALID_CHAR_REGEX = "[ -~]+".toRegex() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt new file mode 100644 index 0000000000..b3f7267580 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt @@ -0,0 +1,54 @@ +/* + * 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 + +/** + * Rooms can claim parents via the m.space.parent state event. + * { + * "type": "m.space.parent", + * "state_key": "!space:example.com", + * "content": { + * "via": ["example.com"], + * "present": true, + * "canonical": true, + * } + * } + */ +@JsonClass(generateAdapter = true) +data class SpaceParentContent( + /** + * Key which gives a list of candidate servers that can be used to join the parent. + * Parents where via is not present are ignored. + */ + @Json(name = "via") val via: List? = null, + /** + * present: true key is included to distinguish from a deleted state event + * Parent where present is not present (sic) or is not set to true are ignored. + */ + @Json(name = "present") val present: Boolean? = false, + /** + * Canonical determines whether this is the main parent for the space. + * When a user joins a room with a canonical parent, clients may switch to view the room + * in the context of that space, peeking into it in order to find other rooms and group them together. + * In practice, well behaved rooms should only have one canonical parent, but given this is not enforced: + * if multiple are present the client should select the one with the lowest room ID, as determined via a lexicographic utf-8 ordering. + */ + @Json(name = "canonical") val canonical: Boolean? = false +) 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 27b2b031e4..b14ed9e9c7 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 @@ -209,8 +209,6 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { val spaceChildInfoSchema = realm.schema.create("SpaceChildInfoEntity") ?.addField(SpaceChildInfoEntityFields.ORDER, String::class.java) - ?.addField(SpaceChildInfoEntityFields.PRESENT, Boolean::class.java) - ?.setNullable(SpaceChildInfoEntityFields.PRESENT, true) ?.addRealmListField(SpaceChildInfoEntityFields.VIA_SERVERS.`$`, String::class.java) ?.addRealmObjectField(SpaceChildInfoEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) 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 index 7128501a65..d08528598d 100644 --- 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 @@ -31,7 +31,6 @@ internal class SpaceSummaryMapper @Inject constructor(private val roomSummaryMap SpaceChildInfo( roomSummary = it.roomSummaryEntity?.let { rs -> roomSummaryMapper.map(rs) }, autoJoin = it.autoJoin ?: false, - present = it.present ?: false, viaServers = it.viaServers.map { it }, order = it.order ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt index 68667c55fc..7862207901 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * 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. @@ -24,8 +24,6 @@ import io.realm.RealmObject */ internal open class SpaceChildInfoEntity( var viaServers: RealmList = RealmList(), - // it's an active child of the space if and only if present is not null and true - var present: Boolean? = null, // Use for alphabetic ordering of this child var order: String? = null, // If true, this child should be join when parent is joined diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt index 9faf50dd8b..b39cbaa582 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt @@ -36,7 +36,11 @@ internal class RoomAliasAvailabilityChecker @Inject constructor( @Throws(RoomAliasError::class) suspend fun check(aliasLocalPart: String?) { if (aliasLocalPart.isNullOrEmpty()) { - throw RoomAliasError.AliasEmpty + // don't check empty or not provided alias + return + } + if (aliasLocalPart.isBlank()) { + throw RoomAliasError.AliasIsBlank } // Check alias availability val fullAlias = aliasLocalPart.toFullLocalAlias(userId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt index 13d403e2e4..a3f6cd53cd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.room.create import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContentOverride import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody @@ -111,5 +111,5 @@ internal data class CreateRoomBody( * The power level content to override in the default power level event */ @Json(name = "power_level_content_override") - val powerLevelContentOverride: PowerLevelsContent? + val powerLevelContentOverride: PowerLevelsContentOverride? ) 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 index 54b9a17c3c..c04f5d3948 100644 --- 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 @@ -41,28 +41,31 @@ internal class RoomRelationshipHelper(private val realm: Realm, data class SpaceChildInfo( val roomId: String, - val present: Boolean, val order: String?, val autoJoin: Boolean, val viaServers: List ) + /** + * Gets the ordered list of valid child description. + */ 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() ContentMapper.map(it.root?.content).toModel()?.let { scc -> Timber.d("## Space child desc state event $scc") - SpaceChildInfo( - roomId = it.stateKey, - present = scc.present ?: false, - order = scc.order, - autoJoin = scc.default ?: false, - viaServers = scc.via ?: emptyList() - ) + // Children where via is not present are ignored. + scc.via?.let { via -> + SpaceChildInfo( + roomId = it.stateKey, + order = scc.validOrder(), + autoJoin = scc.autoJoin ?: false, + viaServers = via + ) + } } } + .sortedBy { it.order } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt index a97709e38b..e5ab87ccac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt @@ -50,7 +50,7 @@ internal fun JsonDict.toSafePowerLevelsContentDict(): JsonDict { eventsDefault = content.eventsDefault, events = content.events, usersDefault = content.usersDefault, - users = content.users, + users = content.users ?: emptyMap(), stateDefault = content.stateDefault, notifications = content.notifications.mapValues { content.notificationLevel(it.key) } ) 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 11d0ffffde..3d01021811 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 @@ -99,6 +99,7 @@ internal class RoomSummaryUpdater @Inject constructor( val roomType = ContentMapper.map(roomCreateEvent?.content).toModel()?.type roomSummaryEntity.roomType = roomType + Timber.v("## Space: Updating summary room [$roomId] 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) @@ -176,7 +177,6 @@ internal class RoomSummaryUpdater @Inject constructor( realm.createObject().apply { this.roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, it.roomId) this.order = it.order - this.present = it.present this.autoJoin = it.autoJoin }.also { Timber.v("## Space: Updating summary for room $roomId with children $it") 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 index ebe845572d..0728c31974 100644 --- 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 @@ -16,8 +16,10 @@ package org.matrix.android.sdk.internal.session.space +import org.matrix.android.sdk.api.query.QueryStringValue 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.events.model.toModel 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 @@ -28,11 +30,34 @@ class DefaultSpace(private val room: Room) : Space { return room } - override suspend fun addRoom(roomId: String) { + override suspend fun addChildren(roomId: String, viaServers: List, order: String?, autoJoin: Boolean) { asRoom().sendStateEvent( eventType = EventType.STATE_SPACE_CHILD, stateKey = roomId, - body = SpaceChildContent(present = true).toContent() + body = SpaceChildContent( + via = viaServers, + autoJoin = autoJoin, + order = order + ).toContent() + ) + } + + override suspend fun removeRoom(roomId: String) { + val existing = asRoom().getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) + .firstOrNull() + ?.content.toModel() + ?: // should we throw here? + return + + // edit state event and set via to null + asRoom().sendStateEvent( + eventType = EventType.STATE_SPACE_CHILD, + stateKey = roomId, + body = SpaceChildContent( + order = existing.order, + via = null, + autoJoin = existing.autoJoin + ).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 index 42f4ed4742..906886d5e8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.space +import android.net.Uri import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.session.events.model.EventType @@ -23,6 +24,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset 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 @@ -66,6 +68,15 @@ internal class DefaultSpaceService @Inject constructor( return createRoomTask.execute(params) } + override suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean): String { + return createSpace(CreateSpaceParams().apply { + this.name = name + this.topic = topic + this.preset = if (isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT + this.avatarUri = avatarUri + }) + } + override fun getSpace(spaceId: String): Space? { return roomGetter.getRoom(spaceId) ?.takeIf { it.roomSummary()?.roomType == RoomType.SPACE } @@ -120,8 +131,7 @@ internal class DefaultSpaceService @Inject constructor( isEncrypted = false ), order = childStateEv?.order, - present = childStateEv?.present ?: false, - autoJoin = childStateEv?.default ?: false, + autoJoin = childStateEv?.autoJoin ?: false, viaServers = childStateEv?.via ?: emptyList() ) } ?: emptyList() @@ -131,30 +141,8 @@ internal class DefaultSpaceService @Inject constructor( override suspend fun joinSpace(spaceIdOrAlias: String, reason: String?, - viaServers: List, - autoJoinChild: List): SpaceService.JoinSpaceResult { - try { - joinSpaceTask.execute(JoinSpaceTask.Params(spaceIdOrAlias, reason, viaServers)) - // TODO partial success - return SpaceService.JoinSpaceResult.Success -// val childJoinFailures = mutableMapOf() -// autoJoinChild.forEach { info -> -// // TODO what if the child is it self a subspace with some default children? -// try { -// joinRoomTask.execute(JoinRoomTask.Params(info.roomIdOrAlias, null, info.viaServers)) -// } catch (failure: Throwable) { -// // TODO, i could already be a member of this room, handle that as it should not be an error in this context -// childJoinFailures[info.roomIdOrAlias] = failure -// } -// } -// return if (childJoinFailures.isEmpty()) { -// SpaceService.JoinSpaceResult.Success -// } else { -// SpaceService.JoinSpaceResult.PartialSuccess(childJoinFailures) -// } - } catch (throwable: Throwable) { - return SpaceService.JoinSpaceResult.Fail(throwable) - } + viaServers: List): SpaceService.JoinSpaceResult { + return joinSpaceTask.execute(JoinSpaceTask.Params(spaceIdOrAlias, reason, viaServers)) } override suspend fun rejectInvite(spaceId: String, reason: String?) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt index 1878d2c0f9..6ee3652761 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt @@ -18,9 +18,9 @@ package org.matrix.android.sdk.internal.session.space import io.realm.RealmConfiguration import kotlinx.coroutines.TimeoutCancellationException -import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields @@ -32,7 +32,7 @@ import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject -internal interface JoinSpaceTask : Task { +internal interface JoinSpaceTask : Task { data class Params( val roomIdOrAlias: String, val reason: String?, @@ -48,13 +48,17 @@ internal class DefaultJoinSpaceTask @Inject constructor( private val spaceSummaryDataSource: SpaceSummaryDataSource ) : JoinSpaceTask { - override suspend fun execute(params: JoinSpaceTask.Params) { + override suspend fun execute(params: JoinSpaceTask.Params): SpaceService.JoinSpaceResult { Timber.v("## Space: > Joining root space ${params.roomIdOrAlias} ...") - joinRoomTask.execute(JoinRoomTask.Params( - params.roomIdOrAlias, - params.reason, - params.viaServers - )) + try { + joinRoomTask.execute(JoinRoomTask.Params( + params.roomIdOrAlias, + params.reason, + params.viaServers + )) + } catch (failure: Throwable) { + return SpaceService.JoinSpaceResult.Fail(failure) + } Timber.v("## Space: < Joining root space done for ${params.roomIdOrAlias}") // we want to wait for sync result to check for auto join rooms @@ -73,19 +77,32 @@ internal class DefaultJoinSpaceTask @Inject constructor( } } catch (exception: TimeoutCancellationException) { Timber.w("## Space: > Error created with timeout") - throw CreateRoomFailure.CreatedWithTimeout + return SpaceService.JoinSpaceResult.PartialSuccess(emptyMap()) } + val errors = HashMap() Timber.v("## Space: > Sync done ...") - // after that i should have the children (? do i nead to paginate to get state) + // after that i should have the children (? do I need to paginate to get state) val summary = spaceSummaryDataSource.getSpaceSummary(params.roomIdOrAlias) Timber.v("## Space: Found space summary Name:[${summary?.roomSummary?.name}] children: ${summary?.children?.size}") summary?.children?.forEach { val childRoomSummary = it.roomSummary ?: return@forEach - Timber.v("## Space: Processing child :[${childRoomSummary.roomId}] present: ${it.present} autoJoin:${it.autoJoin}") - if (it.present && it.autoJoin) { + Timber.v("## Space: Processing child :[${childRoomSummary.roomId}] autoJoin:${it.autoJoin}") + if (it.autoJoin) { // I should try to join as well if (childRoomSummary.roomType == RoomType.SPACE) { + // recursively join auto-joined child of this space? + when (val subspaceJoinResult = this.execute(JoinSpaceTask.Params(it.roomSummary.roomId, null, it.viaServers))) { + SpaceService.JoinSpaceResult.Success -> { + // nop + } + is SpaceService.JoinSpaceResult.Fail -> { + errors[it.roomSummary.roomId] = subspaceJoinResult.error + } + is SpaceService.JoinSpaceResult.PartialSuccess -> { + errors.putAll(subspaceJoinResult.failedRooms) + } + } } else { try { Timber.v("## Space: Joining room child ${childRoomSummary.roomId}") @@ -95,12 +112,18 @@ internal class DefaultJoinSpaceTask @Inject constructor( viaServers = it.viaServers )) } catch (failure: Throwable) { - // todo keep track for partial success + errors[it.roomSummary.roomId] = failure Timber.e("## Space: Failed to join room child ${childRoomSummary.roomId}") } } } } + + return if (errors.isEmpty()) { + SpaceService.JoinSpaceResult.Success + } else { + SpaceService.JoinSpaceResult.PartialSuccess(errors) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt index ba15f2e981..4612d9e142 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * 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. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt index 1214befebd..8faed5f784 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt @@ -75,8 +75,8 @@ internal class DefaultPeekSpaceTask @Inject constructor( val childRoomsIds = stateEvents .filter { it.type == EventType.STATE_SPACE_CHILD && !it.stateKey.isNullOrEmpty() - // Children where present is not present or is not set to true are ignored. - && it.content?.toModel()?.present == true + // Children where via is not present are ignored. + && it.content?.toModel()?.via != null } .map { it.stateKey to it.content?.toModel() } @@ -101,7 +101,7 @@ internal class DefaultPeekSpaceTask @Inject constructor( // can't peek :/ spaceChildResults.add( SpaceChildPeekResult( - childId, childPeek, entry.second?.default, entry.second?.order + childId, childPeek, entry.second?.autoJoin, entry.second?.order ) ) // continue to next child @@ -114,7 +114,7 @@ internal class DefaultPeekSpaceTask @Inject constructor( SpaceSubChildPeekResult( childId, childPeek, - entry.second?.default, + entry.second?.autoJoin, entry.second?.order, peekChildren(childStateEvents, depth + 1, maxDepth) ) @@ -125,7 +125,7 @@ internal class DefaultPeekSpaceTask @Inject constructor( Timber.v("## SPACE_PEEK: room child $entry") spaceChildResults.add( SpaceChildPeekResult( - childId, childPeek, entry.second?.default, entry.second?.order + childId, childPeek, entry.second?.autoJoin, entry.second?.order ) ) } 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 4722e824f6..2b78627d88 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 @@ -48,7 +48,9 @@ enum class Command(val command: String, val parameters: String, @StringRes val d CONFETTI("/confetti", "", R.string.command_confetti), SNOW("/snow", "", R.string.command_snow), CREATE_SPACE("/createspace", " *", R.string.command_description_create_space), - ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space); + ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space), + JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space), + LEAVE_ROOM("/leave", "", R.string.command_description_leave_room); 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 fe5707ec45..9b190d64fe 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 @@ -318,6 +318,18 @@ object CommandParser { rawCommand ) } + Command.JOIN_SPACE.command -> { + val spaceIdOrAlias = textMessage.substring(Command.JOIN_SPACE.command.length).trim() + ParsedCommand.JoinSpace( + spaceIdOrAlias + ) + } + Command.LEAVE_ROOM.command -> { + val spaceIdOrAlias = textMessage.substring(Command.LEAVE_ROOM.command.length).trim() + ParsedCommand.LeaveRoom( + spaceIdOrAlias + ) + } 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 99b0ae7889..d67caac60a 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 @@ -59,4 +59,6 @@ sealed class ParsedCommand { class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand() class CreateSpace(val name: String, val invitees: List) : ParsedCommand() class AddToSpace(val spaceId: String) : ParsedCommand() + class JoinSpace(val spaceIdOrAlias: String) : ParsedCommand() + class LeaveRoom(val roomId: String) : ParsedCommand() } 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 785449236c..8bd0c60c40 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 @@ -831,7 +831,13 @@ class RoomDetailViewModel @AssistedInject constructor( invitedUserIds.addAll(slashCommandResult.invitees) } val spaceId = session.spaceService().createSpace(params) - session.spaceService().getSpace(spaceId)?.addRoom(state.roomId) + session.spaceService().getSpace(spaceId) + ?.addChildren( + state.roomId, + listOf(session.sessionParams.homeServerHost ?: ""), + null, + true + ) } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) } @@ -842,7 +848,37 @@ class RoomDetailViewModel @AssistedInject constructor( is ParsedCommand.AddToSpace -> { viewModelScope.launch(Dispatchers.IO) { try { - session.spaceService().getSpace(slashCommandResult.spaceId)?.addRoom(room.roomId) + session.spaceService().getSpace(slashCommandResult.spaceId) + ?.addChildren( + room.roomId, + listOf(session.sessionParams.homeServerHost ?: ""), + null, + false + ) + } catch (failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) + } + } + _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.JoinSpace -> { + viewModelScope.launch(Dispatchers.IO) { + try { + session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias) + } catch (failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) + } + } + _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.LeaveRoom -> { + viewModelScope.launch(Dispatchers.IO) { + try { + awaitCallback { + session.getRoom(slashCommandResult.roomId)?.leave(null, it) + } } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) } 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 33dc6bc054..a86f06b142 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 @@ -26,8 +26,8 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.raw.wellknown.getElementWellknown @@ -37,6 +37,8 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError +import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset @@ -182,6 +184,15 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr return@withState } + if (state.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public + && state.roomVisibilityType.aliasLocalPart.isBlank()) { + // we require an alias for public rooms + setState { + copy(asyncCreateRoomRequest = Fail(CreateRoomFailure.AliasError(RoomAliasError.AliasIsBlank))) + } + return@withState + } + setState { copy(asyncCreateRoomRequest = Loading()) } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasErrorFormatter.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasErrorFormatter.kt index 7a23a79ab3..43cd460445 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasErrorFormatter.kt @@ -26,7 +26,7 @@ class RoomAliasErrorFormatter @Inject constructor( ) { fun format(roomAliasError: RoomAliasError?): String? { return when (roomAliasError) { - is RoomAliasError.AliasEmpty -> R.string.create_room_alias_empty + is RoomAliasError.AliasIsBlank -> R.string.create_room_alias_empty is RoomAliasError.AliasNotAvailable -> R.string.create_room_alias_already_in_use is RoomAliasError.AliasInvalid -> R.string.create_room_alias_invalid else -> null diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt index f5dd8b76ec..37dde7d287 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt @@ -100,7 +100,7 @@ class SpacePreviewViewModel @AssistedInject constructor( // trigger modal loading _viewEvents.post(SpacePreviewViewEvents.StartJoining) viewModelScope.launch(Dispatchers.IO) { - val joinResult = session.spaceService().joinSpace(initialState.idOrAlias, null, spaceVia, emptyList()) + val joinResult = session.spaceService().joinSpace(initialState.idOrAlias, null, spaceVia) when (joinResult) { SpaceService.JoinSpaceResult.Success, is SpaceService.JoinSpaceResult.PartialSuccess -> { diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index b296c4e5ea..37e6c3bbcc 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3248,6 +3248,9 @@ Event content Create a community + Create a Spcae + Join the Space with the given id + Leave room with given id (or current room if null) Sending Sent From 2cc5c76fb38a3a1e50d18c9096bed981952f4334 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 16 Feb 2021 10:02:33 +0100 Subject: [PATCH 11/97] hide dev commands from completion --- .../command/AutocompleteCommandPresenter.kt | 23 +++++--- .../im/vector/app/features/command/Command.kt | 54 +++++++++---------- vector/src/main/res/values/strings.xml | 2 +- 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt index d121c68557..aabf15aebe 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt @@ -21,10 +21,12 @@ import androidx.recyclerview.widget.RecyclerView import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.RecyclerViewPresenter import im.vector.app.features.command.Command +import im.vector.app.features.settings.VectorPreferences import javax.inject.Inject class AutocompleteCommandPresenter @Inject constructor(context: Context, - private val controller: AutocompleteCommandController) : + private val controller: AutocompleteCommandController, + private val vectorPreferences: VectorPreferences) : RecyclerViewPresenter(context), AutocompleteClickListener { init { @@ -40,13 +42,18 @@ class AutocompleteCommandPresenter @Inject constructor(context: Context, } override fun onQuery(query: CharSequence?) { - val data = Command.values().filter { - if (query.isNullOrEmpty()) { - true - } else { - it.command.startsWith(query, 1, true) - } - } + val data = Command.values() + .filter { + if (it.isDevCommand && !vectorPreferences.developerMode()) { + return@filter false + } + + if (query.isNullOrEmpty()) { + true + } else { + it.command.startsWith(query, 1, true) + } + } controller.setData(data) } 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 2b78627d88..0b210cf298 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 @@ -24,33 +24,33 @@ import im.vector.app.R * the user can write theses messages to perform some actions * the list will be displayed in this order */ -enum class Command(val command: String, val parameters: String, @StringRes val description: Int) { - EMOTE("/me", "", R.string.command_description_emote), - BAN_USER("/ban", " [reason]", R.string.command_description_ban_user), - UNBAN_USER("/unban", " [reason]", R.string.command_description_unban_user), - SET_USER_POWER_LEVEL("/op", " []", R.string.command_description_op_user), - RESET_USER_POWER_LEVEL("/deop", "", R.string.command_description_deop_user), - INVITE("/invite", " [reason]", R.string.command_description_invite_user), - JOIN_ROOM("/join", " [reason]", R.string.command_description_join_room), - PART("/part", " [reason]", R.string.command_description_part_room), - TOPIC("/topic", "", R.string.command_description_topic), - KICK_USER("/kick", " [reason]", R.string.command_description_kick_user), - CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick), - MARKDOWN("/markdown", "", R.string.command_description_markdown), - RAINBOW("/rainbow", "", R.string.command_description_rainbow), - RAINBOW_EMOTE("/rainbowme", "", R.string.command_description_rainbow_emote), - CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token), - SPOILER("/spoiler", "", R.string.command_description_spoiler), - POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll), - SHRUG("/shrug", "", R.string.command_description_shrug), - 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), - CREATE_SPACE("/createspace", " *", R.string.command_description_create_space), - ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space), - JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space), - LEAVE_ROOM("/leave", "", R.string.command_description_leave_room); +enum class Command(val command: String, val parameters: String, @StringRes val description: Int, val isDevCommand: Boolean) { + EMOTE("/me", "", R.string.command_description_emote, false), + BAN_USER("/ban", " [reason]", R.string.command_description_ban_user, false), + UNBAN_USER("/unban", " [reason]", R.string.command_description_unban_user, false), + SET_USER_POWER_LEVEL("/op", " []", R.string.command_description_op_user, false), + RESET_USER_POWER_LEVEL("/deop", "", R.string.command_description_deop_user, false), + INVITE("/invite", " [reason]", R.string.command_description_invite_user, false), + JOIN_ROOM("/join", " [reason]", R.string.command_description_join_room, false), + PART("/part", " [reason]", R.string.command_description_part_room, false), + TOPIC("/topic", "", R.string.command_description_topic, false), + KICK_USER("/kick", " [reason]", R.string.command_description_kick_user, false), + CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick, false), + MARKDOWN("/markdown", "", R.string.command_description_markdown, false), + RAINBOW("/rainbow", "", R.string.command_description_rainbow, false), + RAINBOW_EMOTE("/rainbowme", "", R.string.command_description_rainbow_emote, false), + CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token, false), + SPOILER("/spoiler", "", R.string.command_description_spoiler, false), + POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll, false), + SHRUG("/shrug", "", R.string.command_description_shrug, false), + PLAIN("/plain", "", R.string.command_description_plain, false), + DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, false), + CONFETTI("/confetti", "", R.string.command_confetti, false), + SNOW("/snow", "", R.string.command_snow, false), + CREATE_SPACE("/createspace", " *", R.string.command_description_create_space, true), + ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space, true), + JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true), + LEAVE_ROOM("/leave", "", R.string.command_description_leave_room, true); val length get() = command.length + 1 diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 37e6c3bbcc..84b2262510 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3248,7 +3248,7 @@ Event content Create a community - Create a Spcae + Create a Space Join the Space with the given id Leave room with given id (or current room if null) From 5aa698768a9304a5de4e636d7d3df3ba348aee81 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 16 Feb 2021 10:17:11 +0100 Subject: [PATCH 12/97] Support update of order/autojoin of child --- .../android/sdk/api/session/space/Space.kt | 6 +++ .../sdk/api/session/space/SpaceService.kt | 5 --- .../internal/session/space/DefaultSpace.kt | 43 ++++++++++++++++--- vector/src/main/res/values/strings.xml | 1 - 4 files changed, 42 insertions(+), 13 deletions(-) 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 index 616393d00b..01a0dbc929 100644 --- 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 @@ -26,5 +26,11 @@ interface Space { suspend fun removeRoom(roomId: String) + @Throws + suspend fun setChildrenOrder(roomId: String, order: String?) + + @Throws + suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean) + // fun getChildren() : List } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt index bef3267086..3e30f14748 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -64,11 +64,6 @@ interface SpaceService { fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List - data class ChildAutoJoinInfo( - val roomIdOrAlias: String, - val viaServers: List = emptyList() - ) - sealed class JoinSpaceResult { object Success : JoinSpaceResult() data class Fail(val error: Throwable) : JoinSpaceResult() 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 index 0728c31974..264cfd44ed 100644 --- 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 @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel 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 +import java.lang.IllegalArgumentException class DefaultSpace(private val room: Room) : Space { @@ -61,11 +62,39 @@ class DefaultSpace(private val room: Room) : Space { ) } -// override fun getChildren(): List { -// // asRoom().getStateEvents(setOf(EventType.STATE_SPACE_CHILD)).mapNotNull { -// // // statekeys are the roomIds -// // -// // } -// return emptyList() -// } + override suspend fun setChildrenOrder(roomId: String, order: String?) { + val existing = asRoom().getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) + .firstOrNull() + ?.content.toModel() + ?: throw IllegalArgumentException("$roomId is not a child of this space") + + // edit state event and set via to null + asRoom().sendStateEvent( + eventType = EventType.STATE_SPACE_CHILD, + stateKey = roomId, + body = SpaceChildContent( + order = order, + via = existing.via, + autoJoin = existing.autoJoin + ).toContent() + ) + } + + override suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean) { + val existing = asRoom().getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) + .firstOrNull() + ?.content.toModel() + ?: throw IllegalArgumentException("$roomId is not a child of this space") + + // edit state event and set via to null + asRoom().sendStateEvent( + eventType = EventType.STATE_SPACE_CHILD, + stateKey = roomId, + body = SpaceChildContent( + order = existing.order, + via = existing.via, + autoJoin = autoJoin + ).toContent() + ) + } } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 84b2262510..01adc31bb1 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3247,7 +3247,6 @@ State event sent! Event content - Create a community Create a Space Join the Space with the given id Leave room with given id (or current room if null) From 883f70306f48d32f901795190a657742da03bbd3 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 23 Feb 2021 09:56:46 +0100 Subject: [PATCH 13/97] Rebase Fixes --- .../org/matrix/android/sdk/session/space/SpaceCreationTest.kt | 2 +- .../sdk/api/session/room/powerlevels/PowerLevelsHelper.kt | 2 +- .../sdk/internal/session/room/state/SafePowerLevelContent.kt | 2 +- .../session/room/timeline/TokenChunkEventPersistor.kt | 4 ++-- .../sdk/internal/session/room/uploads/GetUploadsTask.kt | 2 +- .../app/features/spaces/explore/SpaceDirectoryFragment.kt | 2 +- .../app/features/spaces/preview/SpacePreviewViewModel.kt | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt index 90470233a1..2a99594321 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt index 7918376ca9..eb8dabb48e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt @@ -31,7 +31,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { * @return the power level */ fun getUserPowerLevelValue(userId: String): Int { - return powerLevelsContent.users?.get(userId) + return powerLevelsContent.users.get(userId) ?: powerLevelsContent.usersDefault } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt index e5ab87ccac..a97709e38b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt @@ -50,7 +50,7 @@ internal fun JsonDict.toSafePowerLevelsContentDict(): JsonDict { eventsDefault = content.eventsDefault, events = content.events, usersDefault = content.usersDefault, - users = content.users ?: emptyMap(), + users = content.users, stateDefault = content.stateDefault, notifications = content.notifications.mapValues { content.notificationLevel(it.key) } ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index 903aef16df..a7cba2fe99 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -124,7 +124,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri direction: PaginationDirection): Result { monarchy .awaitTransaction { realm -> - Timber.v("Start persisting ${receivedChunk.events?.size} events in $roomId towards $direction") + Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") val nextToken: String? val prevToken: String? @@ -189,7 +189,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri receivedChunk: TokenChunkEvent, currentChunk: ChunkEntity ) { - Timber.v("Add ${receivedChunk.events?.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") + Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") val roomMemberContentsByUser = HashMap() val eventList = receivedChunk.events val stateEvents = receivedChunk.stateEvents diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt index 74b8372b77..028c3e9193 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt @@ -94,7 +94,7 @@ internal class DefaultGetUploadsTask @Inject constructor( nextToken = chunk.end ?: "", hasMore = chunk.hasMore() ) - events = chunk.events ?: emptyList() + events = chunk.events } var uploadEvents = listOf() diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt index 6b67e20405..d776d9ff51 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt @@ -21,7 +21,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Parcelize data class SpaceDirectoryArgs( diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt index 37dde7d287..987884d8c9 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt @@ -78,7 +78,7 @@ class SpacePreviewViewModel @AssistedInject constructor( } } - private fun handleDismissInvite() = withState { state -> + private fun handleDismissInvite() { // Here we need to join the space himself as well as the default rooms in that space // TODO modal loading viewModelScope.launch(Dispatchers.IO) { @@ -108,7 +108,7 @@ class SpacePreviewViewModel @AssistedInject constructor( _viewEvents.post(SpacePreviewViewEvents.JoinSuccess) } is SpaceService.JoinSpaceResult.Fail -> { - _viewEvents.post(SpacePreviewViewEvents.JoinFailure(joinResult.error?.toString())) + _viewEvents.post(SpacePreviewViewEvents.JoinFailure(joinResult.error.toString())) } } } From d8a32298196bf1c4d28dab87bbd587b040a6e54f Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 23 Feb 2021 11:52:29 +0100 Subject: [PATCH 14/97] a11y fixes --- .../res/layout/fragment_space_preview.xml | 37 ++++++++++--------- vector/src/main/res/layout/item_space.xml | 2 + .../main/res/layout/item_space_roomchild.xml | 6 ++- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/vector/src/main/res/layout/fragment_space_preview.xml b/vector/src/main/res/layout/fragment_space_preview.xml index 257797548e..5781ddb570 100644 --- a/vector/src/main/res/layout/fragment_space_preview.xml +++ b/vector/src/main/res/layout/fragment_space_preview.xml @@ -16,25 +16,25 @@ style="@style/VectorToolbarStyle" android:layout_width="match_parent" android:layout_height="?actionBarSize" - app:navigationIcon="@drawable/ic_x_18dp" - android:elevation="4dp"> + android:elevation="4dp" + app:navigationIcon="@drawable/ic_x_18dp"> - - - - - - - - - - - - + + + + + + + + + + + + + android:background="?list_divider_color" /> + android:gravity="center_horizontal" + android:orientation="horizontal"> + android:layout_height="match_parent" + android:layout_marginStart="8dp" /> Date: Mon, 22 Feb 2021 19:59:19 +0100 Subject: [PATCH 15/97] Creation wizard WIP --- vector/src/main/AndroidManifest.xml | 1 + .../im/vector/app/core/di/FragmentModule.kt | 18 ++ .../im/vector/app/core/di/ScreenComponent.kt | 2 + .../form/FormEditableSquareAvatarItem.kt | 80 +++++++++ .../vector/app/features/home/HomeActivity.kt | 4 + .../features/home/HomeActivitySharedAction.kt | 1 + .../features/spaces/SpaceCreationActivity.kt | 135 ++++++++++++++ .../app/features/spaces/SpaceListFragment.kt | 5 + .../features/spaces/SpaceSummaryController.kt | 12 ++ .../features/spaces/SpacesListViewModel.kt | 7 + .../spaces/create/ChooseSpaceTypeFragment.kt | 49 +++++ .../create/CreateSpaceDefaultRoomsFragment.kt | 56 ++++++ .../create/CreateSpaceDetailsFragment.kt | 70 ++++++++ .../spaces/create/CreateSpaceViewModel.kt | 170 ++++++++++++++++++ .../create/SpaceDefaultRoomEpoxyController.kt | 93 ++++++++++ .../create/SpaceDetailEpoxyController.kt | 90 ++++++++++ .../spaces/create/WizardButtonView.kt | 98 ++++++++++ .../src/main/res/drawable/ic_camera_plain.xml | 10 ++ .../src/main/res/drawable/ic_public_room.xml | 13 ++ .../src/main/res/drawable/ic_room_private.xml | 10 ++ .../fragment_space_create_choose_type.xml | 71 ++++++++ ...agment_space_create_generic_epoxy_form.xml | 35 ++++ .../layout/item_editable_square_avatar.xml | 67 +++++++ .../res/layout/view_space_type_button.xml | 68 +++++++ vector/src/main/res/values/attrs.xml | 12 +- vector/src/main/res/values/strings.xml | 18 ++ 26 files changed, 1194 insertions(+), 1 deletion(-) create mode 100644 vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt create mode 100644 vector/src/main/res/drawable/ic_camera_plain.xml create mode 100644 vector/src/main/res/drawable/ic_public_room.xml create mode 100644 vector/src/main/res/drawable/ic_room_private.xml create mode 100644 vector/src/main/res/layout/fragment_space_create_choose_type.xml create mode 100644 vector/src/main/res/layout/fragment_space_create_generic_epoxy_form.xml create mode 100644 vector/src/main/res/layout/item_editable_square_avatar.xml create mode 100644 vector/src/main/res/layout/view_space_type_button.xml diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index e66a123773..205669fb8a 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -274,6 +274,7 @@ + () { + + @EpoxyAttribute + var avatarRenderer: AvatarRenderer? = null + + @EpoxyAttribute + var matrixItem: MatrixItem? = null + + @EpoxyAttribute + var enabled: Boolean = true + + @EpoxyAttribute + var imageUri: Uri? = null + + @EpoxyAttribute + var clickListener: ClickListener? = null + + @EpoxyAttribute + var deleteListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.imageContainer.onClick(clickListener?.takeIf { enabled }) + if (matrixItem != null) { + avatarRenderer?.renderSpace(matrixItem!!, holder.image) + } else { + GlideApp.with(holder.image) + .load(imageUri) + .apply(RequestOptions.circleCropTransform()) + .into(holder.image) + } + holder.delete.isVisible = enabled && (imageUri != null || matrixItem?.avatarUrl?.isNotEmpty() == true) + holder.delete.onClick(deleteListener?.takeIf { enabled }) + } + + override fun unbind(holder: Holder) { + avatarRenderer?.clear(holder.image) + GlideApp.with(holder.image).clear(holder.image) + super.unbind(holder) + } + class Holder : VectorEpoxyHolder() { + val imageContainer by bind(R.id.itemEditableAvatarImageContainer) + val image by bind(R.id.itemEditableAvatarImage) + val delete by bind(R.id.itemEditableAvatarDelete) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 138ffc26f4..982b59263d 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -54,6 +54,7 @@ import im.vector.app.features.popup.VerificationVectorAlert import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity +import im.vector.app.features.spaces.SpaceCreationActivity import im.vector.app.features.spaces.SpacePreviewActivity import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.workers.signout.ServerBackupStatusViewModel @@ -145,6 +146,9 @@ class HomeActivity : is HomeActivitySharedAction.OpenSpacePreview -> { startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId)) } + is HomeActivitySharedAction.AddSpace -> { + startActivity(SpaceCreationActivity.newIntent(this)) + } }.exhaustive } .disposeOnDestroy() diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt index f72354465b..72e3523336 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt @@ -25,5 +25,6 @@ sealed class HomeActivitySharedAction : VectorSharedAction { object OpenDrawer : HomeActivitySharedAction() object CloseDrawer : HomeActivitySharedAction() object OpenGroup : HomeActivitySharedAction() + object AddSpace : HomeActivitySharedAction() data class OpenSpacePreview(val spaceId: String) : HomeActivitySharedAction() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt new file mode 100644 index 0000000000..65c8102020 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import androidx.fragment.app.Fragment +import com.airbnb.mvrx.viewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.toMvRxBundle +import im.vector.app.core.platform.SimpleFragmentActivity +import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment +import im.vector.app.features.spaces.create.CreateSpaceAction +import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment +import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment +import im.vector.app.features.spaces.create.CreateSpaceEvents +import im.vector.app.features.spaces.create.CreateSpaceState +import im.vector.app.features.spaces.create.CreateSpaceViewModel +import javax.inject.Inject + +class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Factory { + + @Inject lateinit var viewModelFactory: CreateSpaceViewModel.Factory + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + val viewModel: CreateSpaceViewModel by viewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (isFirstCreation()) { + when (withState(viewModel) { it.step }) { + CreateSpaceState.Step.ChooseType -> { + navigateToFragment(ChooseSpaceTypeFragment::class.java) + } + CreateSpaceState.Step.SetDetails -> { + navigateToFragment(ChooseSpaceTypeFragment::class.java) + } + CreateSpaceState.Step.AddRooms -> TODO() + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressed() + return true + } + return super.onOptionsItemSelected(item) + } + + override fun initUiAndData() { + super.initUiAndData() + viewModel.subscribe(this) { + renderState(it) + } + + viewModel.observeViewEvents { + when (it) { + CreateSpaceEvents.NavigateToDetails -> { + navigateToFragment(CreateSpaceDetailsFragment::class.java) + } + CreateSpaceEvents.NavigateToChooseType -> { + navigateToFragment(ChooseSpaceTypeFragment::class.java) + } + CreateSpaceEvents.Dismiss -> { + finish() + } + CreateSpaceEvents.NavigateToAddRooms -> { + navigateToFragment(CreateSpaceDefaultRoomsFragment::class.java) + } + } + } + } + + private fun navigateToFragment(fragmentClass: Class) { + val frag = supportFragmentManager.findFragmentByTag(fragmentClass.name) ?: createFragment(fragmentClass, Bundle().toMvRxBundle()) + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) + .replace(R.id.container, + frag, + fragmentClass.name + ) + .commit() + } + + override fun onBackPressed() { + viewModel.handle(CreateSpaceAction.OnBackPressed) + } + + private fun renderState(state: CreateSpaceState) { + val titleRes = when (state.step) { + CreateSpaceState.Step.ChooseType -> R.string.activity_create_space_title + CreateSpaceState.Step.SetDetails -> R.string.your_public_space + CreateSpaceState.Step.AddRooms -> R.string.your_public_space + } + supportActionBar?.let { + it.title = getString(titleRes) + } ?: run { + setTitle(getString(titleRes)) + } + } + + companion object { + fun newIntent(context: Context): Intent { + return Intent(context, SpaceCreationActivity::class.java).apply { + // putExtra(MvRx.KEY_ARG, SpaceDirectoryArgs(spaceId)) + } + } + } + + override fun create(initialState: CreateSpaceState): CreateSpaceViewModel = viewModelFactory.create(initialState) +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt index e14b920b2c..bc1af38473 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt @@ -57,6 +57,7 @@ class SpaceListFragment @Inject constructor( when (it) { is SpaceListViewEvents.OpenSpaceSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenSpacePreview(it.id)) is SpaceListViewEvents.OpenSpace -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup) + is SpaceListViewEvents.AddSpace -> sharedActionViewModel.post(HomeActivitySharedAction.AddSpace) }.exhaustive } } @@ -78,4 +79,8 @@ class SpaceListFragment @Inject constructor( override fun onSpaceSelected(spaceSummary: SpaceSummary) { viewModel.handle(SpaceListAction.SelectSpace(spaceSummary)) } + + override fun onAddSpaceSelected() { + viewModel.handle(SpaceListAction.AddSpace) + } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt index 29d48b4cd1..d1bc3c6e1c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt @@ -19,8 +19,10 @@ package im.vector.app.features.spaces import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericItemHeader +import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.features.grouplist.homeSpaceSummaryItem import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.session.room.model.Membership @@ -103,9 +105,19 @@ class SpaceSummaryController @Inject constructor( } } } + + // Temporary item to create a new Space (will move with final design) + + genericButtonItem { + id("create") + text(stringProvider.getString(R.string.add_space)) + iconRes(R.drawable.ic_add_black) + buttonClickAction(DebouncedClickListener({ callback?.onAddSpaceSelected() })) + } } interface Callback { fun onSpaceSelected(spaceSummary: SpaceSummary) + fun onAddSpaceSelected() } } 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 index cbfa760f56..b8089d6f98 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt @@ -46,6 +46,7 @@ const val ALL_COMMUNITIES_GROUP_ID = "+ALL_COMMUNITIES_GROUP_ID" sealed class SpaceListAction : VectorViewModelAction { data class SelectSpace(val spaceSummary: SpaceSummary) : SpaceListAction() + object AddSpace : SpaceListAction() } /** @@ -54,6 +55,7 @@ sealed class SpaceListAction : VectorViewModelAction { sealed class SpaceListViewEvents : VectorViewEvents { object OpenSpace : SpaceListViewEvents() data class OpenSpaceSummary(val id: String) : SpaceListViewEvents() + object AddSpace : SpaceListViewEvents() } data class SpaceListViewState( @@ -110,6 +112,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp override fun handle(action: SpaceListAction) { when (action) { is SpaceListAction.SelectSpace -> handleSelectSpace(action) + else -> handleAddSpace() } } @@ -136,6 +139,10 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp } } + private fun handleAddSpace() { + _viewEvents.post(SpaceListViewEvents.AddSpace) + } + private fun observeGroupSummaries() { val roomSummaryQueryParams = roomSummaryQueryParams() { memberships = listOf(Membership.JOIN, Membership.INVITE) diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt new file mode 100644 index 0000000000..080bb99ea4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.create + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.activityViewModel +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.DebouncedClickListener +import im.vector.app.databinding.FragmentSpaceCreateChooseTypeBinding +import javax.inject.Inject + +class ChooseSpaceTypeFragment @Inject constructor( + // private val viewModelFactory: CreateSpaceViewModel.Factory, +) : VectorBaseFragment() { + + private val sharedViewModel: CreateSpaceViewModel by activityViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentSpaceCreateChooseTypeBinding.inflate(layoutInflater, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.publicButton.setOnClickListener(DebouncedClickListener({ + sharedViewModel.handle(CreateSpaceAction.SetRoomType(SpaceType.Public)) + })) + + views.privateButton.setOnClickListener(DebouncedClickListener({ + sharedViewModel.handle(CreateSpaceAction.SetRoomType(SpaceType.Private)) + })) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt new file mode 100644 index 0000000000..2dc36f8715 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.create + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.activityViewModel +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentSpaceCreateGenericEpoxyFormBinding +import javax.inject.Inject + +class CreateSpaceDefaultRoomsFragment @Inject constructor( + private val epoxyController: SpaceDefaultRoomEpoxyController +) : VectorBaseFragment(), SpaceDefaultRoomEpoxyController.Listener { + + private val sharedViewModel: CreateSpaceViewModel by activityViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentSpaceCreateGenericEpoxyFormBinding.inflate(layoutInflater, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.recyclerView.configureWith(epoxyController) + epoxyController.listener = this + + sharedViewModel.subscribe(this) { + epoxyController.setData(it) + } + + views.nextButton.debouncedClicks { + sharedViewModel.handle(CreateSpaceAction.NextFromDetails) + } + } + + // ----------------------------- + // Epoxy controller listener methods + // ----------------------------- +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt new file mode 100644 index 0000000000..506f71c92f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.create + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.activityViewModel +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentSpaceCreateGenericEpoxyFormBinding +import javax.inject.Inject + +class CreateSpaceDetailsFragment @Inject constructor( + private val epoxyController: SpaceDetailEpoxyController +) : VectorBaseFragment(), SpaceDetailEpoxyController.Listener { + + private val sharedViewModel: CreateSpaceViewModel by activityViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentSpaceCreateGenericEpoxyFormBinding.inflate(layoutInflater, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.recyclerView.configureWith(epoxyController) + epoxyController.listener = this + + sharedViewModel.subscribe(this) { + epoxyController.setData(it) + } + + views.nextButton.debouncedClicks { + sharedViewModel.handle(CreateSpaceAction.NextFromDetails) + } + } + + // ----------------------------- + // Epoxy controller listener methods + // ----------------------------- + + override fun onAvatarDelete() { + } + + override fun onAvatarChange() { + } + + override fun onNameChange(newName: String) { + sharedViewModel.handle(CreateSpaceAction.NameChanged(newName)) + } + + override fun onTopicChange(newTopic: String) { + sharedViewModel.handle(CreateSpaceAction.TopicChanged(newTopic)) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt new file mode 100644 index 0000000000..aca8452300 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.create + +import android.net.Uri +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.R +import im.vector.app.core.extensions.exhaustive +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 org.matrix.android.sdk.api.session.Session + +data class CreateSpaceState( + val name: String? = null, + val avatarUri: Uri? = null, + val topic: String = "", + val step: Step = Step.ChooseType, + val spaceType: SpaceType? = null, + val nameInlineError : String? = null, + val defaultRooms: List? = null +) : MvRxState { + + enum class Step { + ChooseType, + SetDetails, + AddRooms + } +} + +enum class SpaceType { + Public, + Private +} + +sealed class CreateSpaceAction : VectorViewModelAction { + data class SetRoomType(val type: SpaceType) : CreateSpaceAction() + data class NameChanged(val name: String) : CreateSpaceAction() + data class TopicChanged(val topic: String) : CreateSpaceAction() + object OnBackPressed : CreateSpaceAction() + object NextFromDetails : CreateSpaceAction() +} + +sealed class CreateSpaceEvents : VectorViewEvents { + object NavigateToDetails : CreateSpaceEvents() + object NavigateToChooseType : CreateSpaceEvents() + object NavigateToAddRooms : CreateSpaceEvents() + object Dismiss : CreateSpaceEvents() +} + +class CreateSpaceViewModel @AssistedInject constructor( + @Assisted initialState: CreateSpaceState, + private val session: Session, + private val stringProvider: StringProvider +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory { + fun create(initialState: CreateSpaceState): CreateSpaceViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: CreateSpaceState): CreateSpaceViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + override fun handle(action: CreateSpaceAction) { + when (action) { + is CreateSpaceAction.SetRoomType -> { + setState { + copy( + step = CreateSpaceState.Step.SetDetails, + spaceType = action.type + ) + } + _viewEvents.post(CreateSpaceEvents.NavigateToDetails) + } + is CreateSpaceAction.NameChanged -> { + setState { + copy( + nameInlineError = null, + name = action.name + ) + } + } + is CreateSpaceAction.TopicChanged -> { + setState { + copy( + topic = action.topic + ) + } + } + CreateSpaceAction.OnBackPressed -> { + handleBackNavigation() + } + CreateSpaceAction.NextFromDetails -> { + handleNextFromDetails() + } + }.exhaustive + } + + private fun handleBackNavigation() = withState { state -> + when (state.step) { + CreateSpaceState.Step.ChooseType -> { + _viewEvents.post(CreateSpaceEvents.Dismiss) + } + CreateSpaceState.Step.SetDetails -> { + setState { + copy( + step = CreateSpaceState.Step.ChooseType + ) + } + _viewEvents.post(CreateSpaceEvents.NavigateToChooseType) + } + CreateSpaceState.Step.AddRooms -> { + setState { + copy( + step = CreateSpaceState.Step.SetDetails + ) + } + _viewEvents.post(CreateSpaceEvents.NavigateToDetails) + } + } + } + + private fun handleNextFromDetails() = withState { state -> + if (state.name.isNullOrBlank()) { + setState { + copy( + nameInlineError = stringProvider.getString(R.string.create_space_error_empty_field_space_name) + ) + } + } else { + setState { + copy( + step = CreateSpaceState.Step.AddRooms + ) + } + _viewEvents.post(CreateSpaceEvents.NavigateToAddRooms) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt new file mode 100644 index 0000000000..05abcf95b0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.create + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.GenericItem +import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.features.form.formEditTextItem +import javax.inject.Inject + +class SpaceDefaultRoomEpoxyController @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider +// private val avatarRenderer: AvatarRenderer +) : TypedEpoxyController() { + + var listener: Listener? = null + + override fun buildModels(data: CreateSpaceState?) { + genericFooterItem { + id("info_help_header") + style(GenericItem.STYLE.BIG_TEXT) + text(stringProvider.getString(R.string.create_spaces_room_public_header)) + textColor(colorProvider.getColorFromAttribute(R.attr.riot_primary_text_color)) + } + + genericFooterItem { + id("info_help") + text(stringProvider.getString(R.string.create_spaces_room_public_header_desc)) + textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)) + } + + formEditTextItem { + id("roomName1") + enabled(true) + value(data?.name) + hint(stringProvider.getString(R.string.create_room_name_hint)) + showBottomSeparator(false) +// errorMessage(data?.nameInlineError) + onTextChange { text -> +// listener?.onNameChange(text) + } + } + + formEditTextItem { + id("roomName2") + enabled(true) +// value(data?.name) + hint(stringProvider.getString(R.string.create_room_name_hint)) + showBottomSeparator(false) +// errorMessage(data?.nameInlineError) + onTextChange { text -> +// listener?.onNameChange(text) + } + } + + formEditTextItem { + id("roomName3") + enabled(true) +// value(data?.name) + hint(stringProvider.getString(R.string.create_room_name_hint)) + showBottomSeparator(false) +// errorMessage(data?.nameInlineError) + onTextChange { text -> +// listener?.onNameChange(text) + } + } + } + + interface Listener { +// fun onAvatarDelete() +// fun onAvatarChange() +// fun onNameChange(newName: String) +// fun onTopicChange(newTopic: String) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt new file mode 100644 index 0000000000..357b741ff4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.create + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.features.form.formEditTextItem +import im.vector.app.features.form.formEditableSquareAvatarItem +import im.vector.app.features.form.formMultiLineEditTextItem +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.MatrixItem +import javax.inject.Inject + +class SpaceDetailEpoxyController @Inject constructor( + private val stringProvider: StringProvider, + private val avatarRenderer: AvatarRenderer +) : TypedEpoxyController() { + + var listener: Listener? = null + + override fun buildModels(data: CreateSpaceState?) { + genericFooterItem { + id("info_help") + text( + if (data?.spaceType == SpaceType.Public) { + stringProvider.getString(R.string.create_spaces_details_public_header) + } else { + stringProvider.getString(R.string.create_spaces_details_private_header) + } + ) + } + + formEditableSquareAvatarItem { + id("avatar") + enabled(true) + imageUri(data?.avatarUri) + avatarRenderer(avatarRenderer) + matrixItem(data?.name?.let { MatrixItem.RoomItem("", it, null).takeIf { !it.displayName.isNullOrBlank() } }) + clickListener { listener?.onAvatarChange() } + deleteListener { listener?.onAvatarDelete() } + } + + formEditTextItem { + id("name") + enabled(true) + value(data?.name) + hint(stringProvider.getString(R.string.create_room_name_hint)) + showBottomSeparator(false) + errorMessage(data?.nameInlineError) + onTextChange { text -> + listener?.onNameChange(text) + } + } + + formMultiLineEditTextItem { + id("topic") + enabled(true) + value(data?.topic) + hint(stringProvider.getString(R.string.create_room_topic_hint)) + showBottomSeparator(false) + textSizeSp(15) + onTextChange { text -> + listener?.onTopicChange(text) + } + } + } + + interface Listener { + fun onAvatarDelete() + fun onAvatarChange() + fun onNameChange(newName: String) + fun onTopicChange(newTopic: String) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt b/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt new file mode 100644 index 0000000000..552a98ded2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.create + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.util.TypedValue +import androidx.appcompat.content.res.AppCompatResources.getDrawable +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.withStyledAttributes +import im.vector.app.R +import im.vector.app.databinding.ViewSpaceTypeButtonBinding + +class WizardButtonView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) + : ConstraintLayout(context, attrs, defStyle) { + + private val views: ViewSpaceTypeButtonBinding + + var title: String? = null + set(value) { + if (value != title) { + field = value + views.title.text = value + } + } + + var subTitle: String? = null + set(value) { + if (value != subTitle) { + field = value + views.subTitle.text = value + } + } + + var icon: Drawable? = null + set(value) { + if (value != icon) { + field = value + views.buttonImageView.setImageDrawable(value) + } + } + +// private var tint: Int? = null +// set(value) { +// field = value +// if (value != null) { +// views.buttonImageView.imageTintList = ColorStateList.valueOf(value) +// } else { +// views.buttonImageView.clearColorFilter() +// } +// } + +// var action: (() -> Unit)? = null + + init { + inflate(context, R.layout.view_space_type_button, this) + views = ViewSpaceTypeButtonBinding.bind(this) + + if (isInEditMode) { + title = "Title" + subTitle = "This is doing something" + } + + context.withStyledAttributes(attrs, R.styleable.WizardButtonView) { + title = getString(R.styleable.WizardButtonView_title) ?: "" + subTitle = getString(R.styleable.WizardButtonView_subTitle) ?: "" + icon = getDrawable(R.styleable.WizardButtonView_icon) +// tint = getColor(R.styleable.WizardButtonView_iconTint, ThemeUtils.getColor(context, R.attr.riotx_text_primary)) + } + + val outValue = TypedValue() + context.theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + this.foreground = getDrawable(context, outValue.resourceId) + } + +// views.content.isClickable = true +// views.content.isFocusable = true +// views.content.setOnClickListener { +// action?.invoke() +// } + } +} diff --git a/vector/src/main/res/drawable/ic_camera_plain.xml b/vector/src/main/res/drawable/ic_camera_plain.xml new file mode 100644 index 0000000000..56d55c9da1 --- /dev/null +++ b/vector/src/main/res/drawable/ic_camera_plain.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_public_room.xml b/vector/src/main/res/drawable/ic_public_room.xml new file mode 100644 index 0000000000..1520898831 --- /dev/null +++ b/vector/src/main/res/drawable/ic_public_room.xml @@ -0,0 +1,13 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_room_private.xml b/vector/src/main/res/drawable/ic_room_private.xml new file mode 100644 index 0000000000..cacdf15a3b --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_private.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/fragment_space_create_choose_type.xml b/vector/src/main/res/layout/fragment_space_create_choose_type.xml new file mode 100644 index 0000000000..ddf61aabf8 --- /dev/null +++ b/vector/src/main/res/layout/fragment_space_create_choose_type.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_space_create_generic_epoxy_form.xml b/vector/src/main/res/layout/fragment_space_create_generic_epoxy_form.xml new file mode 100644 index 0000000000..3097664e02 --- /dev/null +++ b/vector/src/main/res/layout/fragment_space_create_generic_epoxy_form.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_editable_square_avatar.xml b/vector/src/main/res/layout/item_editable_square_avatar.xml new file mode 100644 index 0000000000..b3ec057fd4 --- /dev/null +++ b/vector/src/main/res/layout/item_editable_square_avatar.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/view_space_type_button.xml b/vector/src/main/res/layout/view_space_type_button.xml new file mode 100644 index 0000000000..1ccbed3201 --- /dev/null +++ b/vector/src/main/res/layout/view_space_type_button.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index ecc64bf07d..86105cf74d 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -54,8 +54,10 @@ + + - + @@ -68,4 +70,12 @@ + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 01adc31bb1..b2bfe76f57 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3259,4 +3259,22 @@ Messages failed to send Delete unsent messages Are you sure you want to delete all unsent messages in this room? + + Add Space + Your public space + Spaces are a new way to group rooms and people + What type of space do you want to create? + To join an existing space, you need an invite. + Public + Open to anyone, best for communities + Private + Invite only, best for yourself or teams + Create a space + Add some details to help it stand out. You can change these at any point. + Add some details to help people identify it. You can change these at any point. + Give it a name to continue. + What are some discussions you want to have in Runner’s World? + We’ll create rooms for them, and auto-join everyone. You can add more later too. + + From 6c69a6055da7fa45f4a7702696a76c7366b64707 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 25 Feb 2021 00:17:27 +0100 Subject: [PATCH 16/97] Support retry after M_LIMIT_EXCEEDED --- .../sdk/internal/task/ConfigurableTask.kt | 6 +++-- .../matrix/android/sdk/internal/task/Task.kt | 22 +++++++++++++++++++ .../android/sdk/internal/task/TaskExecutor.kt | 6 ++--- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt index 97f9a0dd51..bc80cf7ee8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt @@ -37,7 +37,8 @@ internal data class ConfigurableTask( val id: UUID, val callbackThread: TaskThread, val executionThread: TaskThread, - val callback: MatrixCallback + val callback: MatrixCallback, + val maxRetryCount: Int = 0 ) : Task by task { @@ -57,7 +58,8 @@ internal data class ConfigurableTask( id = id, callbackThread = callbackThread, executionThread = executionThread, - callback = callback + callback = callback, + maxRetryCount = retryCount ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt index a6c80a0b1a..a5d031e02a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt @@ -16,7 +16,29 @@ package org.matrix.android.sdk.internal.task +import kotlinx.coroutines.delay +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.shouldBeRetried +import timber.log.Timber + internal interface Task { suspend fun execute(params: PARAMS): RESULT + + suspend fun executeRetry(params: PARAMS, remainingRetry: Int) : RESULT { + return try { + execute(params) + } catch (failure: Throwable) { + if (failure.shouldBeRetried() && remainingRetry > 0) { + Timber.d(failure, "## TASK: Retriable error") + if (failure is Failure.ServerError) { + val waitTime = failure.error.retryAfterMillis ?: 0L + Timber.d(failure, "## TASK: Quota wait time $waitTime") + delay(waitTime + 100) + } + return executeRetry(params, remainingRetry - 1) + } + throw failure + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt index 478a356432..4da16eff22 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt @@ -40,9 +40,9 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers .launch(task.callbackThread.toDispatcher()) { val resultOrFailure = runCatching { withContext(task.executionThread.toDispatcher()) { - Timber.v("Enqueue task $task") - Timber.v("Execute task $task on ${Thread.currentThread().name}") - task.execute(task.params) + Timber.v("## TASK: Enqueue task $task") + Timber.v("## TASK: Execute task $task on ${Thread.currentThread().name}") + task.executeRetry(task.params, task.maxRetryCount) } } resultOrFailure From 7d2d7b411e0efc7069932cafe4b7a1549d08dd2b Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 25 Feb 2021 00:23:53 +0100 Subject: [PATCH 17/97] Space Create Wizard Flow --- .../session/room/failure/CreateRoomFailure.kt | 2 +- .../android/sdk/api/session/space/Space.kt | 6 ++ .../session/room/DefaultRoomService.kt | 9 +- .../sdk/internal/session/room/SpaceGetter.kt | 39 ++++++++ .../session/room/create/CreateRoomTask.kt | 2 +- .../session/room/state/DefaultStateService.kt | 2 +- .../internal/session/space/CreateSpaceTask.kt | 56 +++++++++++ .../internal/session/space/DefaultSpace.kt | 8 +- .../session/space/DefaultSpaceService.kt | 38 +++----- .../sdk/internal/session/space/SpaceModule.kt | 8 ++ .../app/features/form/FormEditTextItem.kt | 9 ++ .../form/FormEditableSquareAvatarItem.kt | 31 ++++-- .../grouplist/HomeSpaceSummaryItem.kt | 4 + .../vector/app/features/home/HomeActivity.kt | 21 +++- .../features/navigation/DefaultNavigator.kt | 21 ++++ .../app/features/navigation/Navigator.kt | 2 + .../features/spaces/SpaceCreationActivity.kt | 29 +++++- .../features/spaces/SpacesListViewModel.kt | 15 ++- .../spaces/create/ChooseSpaceTypeFragment.kt | 2 +- .../create/CreateSpaceDefaultRoomsFragment.kt | 6 +- .../create/CreateSpaceDetailsFragment.kt | 16 +++- .../spaces/create/CreateSpaceViewModel.kt | 94 +++++++++++++++++- .../spaces/create/CreateSpaceViewModelTask.kt | 95 +++++++++++++++++++ .../create/SpaceDefaultRoomEpoxyController.kt | 26 +++-- .../spaces/create/WizardButtonView.kt | 3 +- vector/src/main/res/values/ids.xml | 3 + vector/src/main/res/values/strings.xml | 2 + 27 files changed, 481 insertions(+), 68 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/CreateSpaceTask.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt create mode 100644 vector/src/main/res/values/ids.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt index 208cdd4556..deab0ca3e7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt @@ -21,7 +21,7 @@ import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.session.room.alias.RoomAliasError sealed class CreateRoomFailure : Failure.FeatureFailure() { - object CreatedWithTimeout : CreateRoomFailure() + data class CreatedWithTimeout(val roomID: String) : CreateRoomFailure() data class CreatedWithFederationFailure(val matrixError: MatrixError) : CreateRoomFailure() data class AliasError(val aliasError: RoomAliasError) : CreateRoomFailure() } 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 index 01a0dbc929..0172b3701b 100644 --- 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 @@ -17,11 +17,17 @@ package org.matrix.android.sdk.api.session.space import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.RoomSummary interface Space { fun asRoom() : Room + /** + * A current snapshot of [RoomSummary] associated with the room + */ + fun spaceSummary(): SpaceSummary? + suspend fun addChildren(roomId: String, viaServers: List, order: String?, autoJoin: Boolean = false) suspend fun removeRoom(roomId: String) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 22f61bc517..4724167e87 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -66,8 +66,13 @@ internal class DefaultRoomService @Inject constructor( private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource ) : RoomService { - override suspend fun createRoom(createRoomParams: CreateRoomParams): String { - return createRoomTask.execute(createRoomParams) + override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable { + return createRoomTask + .configureWith(createRoomParams) { + this.callback = callback + this.retryCount = 3 + } + .executeBy(taskExecutor) } override fun getRoom(roomId: String): Room? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt new file mode 100644 index 0000000000..f440a67710 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.space.Space +import org.matrix.android.sdk.internal.session.space.DefaultSpace +import org.matrix.android.sdk.internal.session.space.SpaceSummaryDataSource +import javax.inject.Inject + +internal interface SpaceGetter { + fun get(spaceId: String): Space? +} + +internal class DefaultSpaceGetter @Inject constructor( + private val roomGetter: RoomGetter, + private val spaceSummaryDataSource: SpaceSummaryDataSource +) : SpaceGetter { + + override fun get(spaceId: String): Space? { + return roomGetter.getRoom(spaceId) + ?.takeIf { it.roomSummary()?.roomType == RoomType.SPACE } + ?.let { DefaultSpace(it, spaceSummaryDataSource) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt index bafe2b90ae..de6a71e581 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt @@ -102,7 +102,7 @@ internal class DefaultCreateRoomTask @Inject constructor( .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) } } catch (exception: TimeoutCancellationException) { - throw CreateRoomFailure.CreatedWithTimeout + throw CreateRoomFailure.CreatedWithTimeout(roomId) } Realm.getInstance(realmConfiguration).executeTransactionAsync { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index 615bc99096..4948e3ace5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -73,7 +73,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private eventType = eventType, body = body.toSafeJson(eventType) ) - sendStateTask.execute(params) + sendStateTask.executeRetry(params, 3) } private fun JsonDict.toSafeJson(eventType: String): JsonDict { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/CreateSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/CreateSpaceTask.kt new file mode 100644 index 0000000000..5f174587d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/CreateSpaceTask.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.space + +import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException +import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult +import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity +import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask +import org.matrix.android.sdk.internal.task.Task +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * A simple wrapper of create room task that adds waiting for DB entities of spaces + */ +internal interface CreateSpaceTask : Task + +internal class DefaultCreateSpaceTask @Inject constructor( + private val createRoomTask: CreateRoomTask, + @SessionDatabase private val realmConfiguration: RealmConfiguration +) : CreateSpaceTask { + + override suspend fun execute(params: CreateRoomParams): String { + val spaceId = createRoomTask.execute(params) + + try { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(SpaceSummaryEntity::class.java) + .equalTo(SpaceSummaryEntityFields.SPACE_ID, spaceId) + } + } catch (exception: TimeoutCancellationException) { + throw CreateRoomFailure.CreatedWithTimeout(spaceId) + } + + return spaceId + } +} 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 index 264cfd44ed..efba103ab7 100644 --- 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 @@ -22,15 +22,19 @@ import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel 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.SpaceSummary import org.matrix.android.sdk.api.session.space.model.SpaceChildContent -import java.lang.IllegalArgumentException -class DefaultSpace(private val room: Room) : Space { +internal class DefaultSpace(private val room: Room, private val spaceSummaryDataSource: SpaceSummaryDataSource) : Space { override fun asRoom(): Room { return room } + override fun spaceSummary(): SpaceSummary? { + return spaceSummaryDataSource.getSpaceSummary(asRoom().roomId) + } + override suspend fun addChildren(roomId: String, viaServers: List, order: String?, autoJoin: Boolean) { asRoom().sendStateEvent( eventType = EventType.STATE_SPACE_CHILD, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index 906886d5e8..0210c81e5d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -22,7 +22,6 @@ import com.zhuinden.monarchy.Monarchy 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.room.model.RoomSummary -import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.api.session.space.CreateSpaceParams @@ -32,40 +31,33 @@ 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.space.model.SpaceChildContent 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.SpaceGetter import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask -import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult -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 createSpaceTask: CreateSpaceTask, +// private val joinRoomTask: JoinRoomTask, private val joinSpaceTask: JoinSpaceTask, - private val markAllRoomsReadTask: MarkAllRoomsReadTask, - private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, - private val roomIdByAliasTask: GetRoomIdByAliasTask, - private val deleteRoomAliasTask: DeleteRoomAliasTask, - private val roomGetter: RoomGetter, + private val spaceGetter: SpaceGetter, +// 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 peekSpaceTask: PeekSpaceTask, private val resolveSpaceInfoTask: ResolveSpaceInfoTask, - private val leaveRoomTask: LeaveRoomTask, - private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, - private val taskExecutor: TaskExecutor + private val leaveRoomTask: LeaveRoomTask +// private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, +// private val taskExecutor: TaskExecutor ) : SpaceService { override suspend fun createSpace(params: CreateSpaceParams): String { - return createRoomTask.execute(params) + return createSpaceTask.executeRetry(params, 3) } override suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean): String { @@ -78,9 +70,7 @@ internal class DefaultSpaceService @Inject constructor( } override fun getSpace(spaceId: String): Space? { - return roomGetter.getRoom(spaceId) - ?.takeIf { it.roomSummary()?.roomType == RoomType.SPACE } - ?.let { DefaultSpace(it) } + return spaceGetter.get(spaceId) } override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt index 4612d9e142..84a2d5267f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt @@ -20,6 +20,8 @@ import dagger.Binds import dagger.Module import dagger.Provides import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.DefaultSpaceGetter +import org.matrix.android.sdk.internal.session.room.SpaceGetter import org.matrix.android.sdk.internal.session.space.peeking.DefaultPeekSpaceTask import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask import retrofit2.Retrofit @@ -45,4 +47,10 @@ internal abstract class SpaceModule { @Binds abstract fun bindJoinSpaceTask(task: DefaultJoinSpaceTask): JoinSpaceTask + + @Binds + abstract fun bindSpaceGetter(getter: DefaultSpaceGetter): SpaceGetter + + @Binds + abstract fun bindCreateSpaceTask(getter: DefaultCreateSpaceTask): CreateSpaceTask } diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt index 68e2e6b371..a1bd2bd1a3 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt @@ -18,6 +18,7 @@ package im.vector.app.features.form import android.text.Editable import android.view.View +import android.view.inputmethod.EditorInfo import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass @@ -50,6 +51,12 @@ abstract class FormEditTextItem : VectorEpoxyModel() { @EpoxyAttribute var inputType: Int? = null + @EpoxyAttribute + var singleLine: Boolean? = null + + @EpoxyAttribute + var imeOptions: Int? = null + @EpoxyAttribute var onTextChange: ((String) -> Unit)? = null @@ -69,6 +76,8 @@ abstract class FormEditTextItem : VectorEpoxyModel() { holder.textInputEditText.setTextSafe(value) holder.textInputEditText.isEnabled = enabled inputType?.let { holder.textInputEditText.inputType = it } + holder.textInputEditText.isSingleLine = singleLine ?: false + holder.textInputEditText.imeOptions = imeOptions ?: EditorInfo.IME_ACTION_NONE holder.textInputEditText.addTextChangedListener(onTextChangeListener) holder.bottomSeparator.isVisible = showBottomSeparator diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt index 5023c1b483..cbb545825d 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt @@ -16,13 +16,16 @@ package im.vector.app.features.form import android.net.Uri +import android.util.TypedValue import android.view.View import android.widget.ImageView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelWithHolder -import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.load.MultiTransformation +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder @@ -55,13 +58,24 @@ abstract class FormEditableSquareAvatarItem : EpoxyModelWithHolder { + val corner = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 8f, + holder.view.resources.displayMetrics + ).toInt() + GlideApp.with(holder.image) + .load(imageUri) + .transform(MultiTransformation(CenterCrop(), RoundedCorners(corner))) + .into(holder.image) + } + matrixItem != null -> { + avatarRenderer?.renderSpace(matrixItem!!, holder.image) + } + else -> { + avatarRenderer?.clear(holder.image) + } } holder.delete.isVisible = enabled && (imageUri != null || matrixItem?.avatarUrl?.isNotEmpty() == true) holder.delete.onClick(deleteListener?.takeIf { enabled }) @@ -72,6 +86,7 @@ abstract class FormEditableSquareAvatarItem : EpoxyModelWithHolder(R.id.itemEditableAvatarImageContainer) val image by bind(R.id.itemEditableAvatarImage) 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 index ade86a9d89..c3f459f49e 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt @@ -35,6 +35,10 @@ abstract class HomeSpaceSummaryItem : VectorEpoxyModel Unit)? = null + override fun getViewType(): Int { + // mm.. it's reusing the same layout for basic space item + return R.id.space_item_home + } override fun bind(holder: Holder) { super.bind(holder) holder.rootView.setOnClickListener { listener?.invoke() } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 982b59263d..40d0cdd622 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -16,6 +16,7 @@ package im.vector.app.features.home +import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri @@ -36,6 +37,7 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity @@ -103,6 +105,23 @@ class HomeActivity : @Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter + private val createSpaceResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + val spaceId = activityResult.data?.extras?.getString(SpaceCreationActivity.RESULT_DATA_CREATED_SPACE_ID) + val defaultRoomsId = activityResult.data?.extras?.getString(SpaceCreationActivity.RESULT_DATA_DEFAULT_ROOM_ID) + views.drawerLayout.closeDrawer(GravityCompat.START) + + // Here we want to change current space to the newly created one, and then immediatly open the default room + if (spaceId != null) { + navigator.switchToSpace(this, spaceId, defaultRoomsId) + } + + // Also we should show the share space bottomsheet + } else { + // viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled) + } + } + private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { hideKeyboard() @@ -147,7 +166,7 @@ class HomeActivity : startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId)) } is HomeActivitySharedAction.AddSpace -> { - startActivity(SpaceCreationActivity.newIntent(this)) + createSpaceResultLauncher.launch(SpaceCreationActivity.newIntent(this)) } }.exhaustive } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 76e4cad28f..d6eeade97e 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -29,6 +29,7 @@ import androidx.core.app.ActivityOptionsCompat import androidx.core.app.TaskStackBuilder import androidx.core.util.Pair import androidx.core.view.ViewCompat +import arrow.core.Option import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.error.fatalError @@ -46,6 +47,7 @@ import im.vector.app.features.crypto.verification.SupportedVerificationMethodsPr import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.debug.DebugMenuActivity import im.vector.app.features.devtools.RoomDevToolActivity +import im.vector.app.features.grouplist.SelectedSpaceDataSource import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import im.vector.app.features.home.room.detail.search.SearchActivity @@ -77,6 +79,7 @@ import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryDat import org.matrix.android.sdk.api.session.terms.TermsService import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -85,6 +88,7 @@ class DefaultNavigator @Inject constructor( private val sessionHolder: ActiveSessionHolder, private val vectorPreferences: VectorPreferences, private val widgetArgsBuilder: WidgetArgsBuilder, + private val selectedSpaceDataSource: SelectedSpaceDataSource, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider ) : Navigator { @@ -98,6 +102,23 @@ class DefaultNavigator @Inject constructor( startActivity(context, intent, buildTask) } + override fun switchToSpace(context: Context, spaceId: String, roomId: String?) { + if (sessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(spaceId) == null) { + fatalError("Trying to open an unknown space $spaceId", vectorPreferences.failFast()) + return + } + + sessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(spaceId)?.spaceSummary()?.let { + Timber.d("## Nav: Switching to space $spaceId / ${it.roomSummary.name}") + selectedSpaceDataSource.post(Option.just(it)) + } ?: kotlin.run { + Timber.d("## Nav: Failed to switch to space $spaceId") + } + if (roomId != null) { + openRoom(context, roomId) + } + } + override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) { val session = sessionHolder.getSafeActiveSession() ?: return val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index b4bd677b0c..fcd35be162 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -38,6 +38,8 @@ interface Navigator { fun openRoom(context: Context, roomId: String, eventId: String? = null, buildTask: Boolean = false) + fun switchToSpace(context: Context, spaceId: String, roomId: String?) + fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) fun requestSessionVerification(context: Context, otherSessionId: String) diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt index 65c8102020..ed8b85f587 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt @@ -20,7 +20,9 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem +import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState import im.vector.app.R @@ -91,6 +93,23 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac CreateSpaceEvents.NavigateToAddRooms -> { navigateToFragment(CreateSpaceDefaultRoomsFragment::class.java) } + is CreateSpaceEvents.ShowModalError -> { + hideWaitingView() + AlertDialog.Builder(this) + .setMessage(it.errorMessage) + .setPositiveButton(getString(R.string.ok), null) + .show() + } + is CreateSpaceEvents.FinishSuccess -> { + setResult(RESULT_OK, Intent().apply { + putExtra(RESULT_DATA_CREATED_SPACE_ID, it.spaceId) + putExtra(RESULT_DATA_DEFAULT_ROOM_ID, it.defaultRoomId) + }) + finish() + } + CreateSpaceEvents.HideModalLoading -> { + hideWaitingView() + } } } } @@ -114,16 +133,24 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac val titleRes = when (state.step) { CreateSpaceState.Step.ChooseType -> R.string.activity_create_space_title CreateSpaceState.Step.SetDetails -> R.string.your_public_space - CreateSpaceState.Step.AddRooms -> R.string.your_public_space + CreateSpaceState.Step.AddRooms -> R.string.your_public_space } supportActionBar?.let { it.title = getString(titleRes) } ?: run { setTitle(getString(titleRes)) } + + if (state.creationResult is Loading) { + showWaitingView(getString(R.string.create_spaces_loading_message)) + } } companion object { + + const val RESULT_DATA_CREATED_SPACE_ID = "RESULT_DATA_CREATED_SPACE_ID" + const val RESULT_DATA_DEFAULT_ROOM_ID = "RESULT_DATA_DEFAULT_ROOM_ID" + fun newIntent(context: Context): Intent { return Intent(context, SpaceCreationActivity::class.java).apply { // putExtra(MvRx.KEY_ARG, SpaceDirectoryArgs(spaceId)) 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 index b8089d6f98..4ec6e7d4ad 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt @@ -86,8 +86,15 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp private var currentGroupId = "" init { - observeGroupSummaries() + observeSpaceSummaries() observeSelectionState() + selectedSpaceDataSource.observe().execute { + if (this.selectedSpace != it.invoke()?.orNull()) { + copy( + selectedSpace = it.invoke()?.orNull() + ) + } else this + } } private fun observeSelectionState() { @@ -143,8 +150,8 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp _viewEvents.post(SpaceListViewEvents.AddSpace) } - private fun observeGroupSummaries() { - val roomSummaryQueryParams = roomSummaryQueryParams() { + private fun observeSpaceSummaries() { + val spaceSummaryQueryParams = roomSummaryQueryParams() { memberships = listOf(Membership.JOIN, Membership.INVITE) displayName = QueryStringValue.IsNotEmpty excludeType = listOf(/**RoomType.MESSAGING,$*/ @@ -171,7 +178,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp }, session .rx() - .liveSpaceSummaries(roomSummaryQueryParams), + .liveSpaceSummaries(spaceSummaryQueryParams), BiFunction { allCommunityGroup, communityGroups -> listOf(allCommunityGroup) + communityGroups } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt index 080bb99ea4..b174952ec6 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt @@ -43,7 +43,7 @@ class ChooseSpaceTypeFragment @Inject constructor( })) views.privateButton.setOnClickListener(DebouncedClickListener({ - sharedViewModel.handle(CreateSpaceAction.SetRoomType(SpaceType.Private)) + // sharedViewModel.handle(CreateSpaceAction.SetRoomType(SpaceType.Private)) })) } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt index 2dc36f8715..fb1ed8e5f8 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt @@ -46,10 +46,14 @@ class CreateSpaceDefaultRoomsFragment @Inject constructor( } views.nextButton.debouncedClicks { - sharedViewModel.handle(CreateSpaceAction.NextFromDetails) + sharedViewModel.handle(CreateSpaceAction.NextFromDefaultRooms) } } + override fun onNameChange(index: Int, newName: String) { + sharedViewModel.handle(CreateSpaceAction.DefaultRoomNameChanged(index, newName)) + } + // ----------------------------- // Epoxy controller listener methods // ----------------------------- diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt index 506f71c92f..670876fdf1 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt @@ -16,25 +16,32 @@ package im.vector.app.features.spaces.create +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.airbnb.mvrx.activityViewModel +import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.ColorProvider import im.vector.app.databinding.FragmentSpaceCreateGenericEpoxyFormBinding import javax.inject.Inject class CreateSpaceDetailsFragment @Inject constructor( - private val epoxyController: SpaceDetailEpoxyController -) : VectorBaseFragment(), SpaceDetailEpoxyController.Listener { + private val epoxyController: SpaceDetailEpoxyController, + private val colorProvider: ColorProvider +) : VectorBaseFragment(), SpaceDetailEpoxyController.Listener, + GalleryOrCameraDialogHelper.Listener { private val sharedViewModel: CreateSpaceViewModel by activityViewModel() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentSpaceCreateGenericEpoxyFormBinding.inflate(layoutInflater, container, false) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -50,14 +57,19 @@ class CreateSpaceDetailsFragment @Inject constructor( } } + override fun onImageReady(uri: Uri?) { + sharedViewModel.handle(CreateSpaceAction.SetAvatar(uri)) + } // ----------------------------- // Epoxy controller listener methods // ----------------------------- override fun onAvatarDelete() { + sharedViewModel.handle(CreateSpaceAction.SetAvatar(null)) } override fun onAvatarChange() { + galleryOrCameraDialogHelper.show() } override fun onNameChange(newName: String) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt index aca8452300..cd2680ef5f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt @@ -17,20 +17,29 @@ package im.vector.app.features.spaces.create import android.net.Uri +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.exhaustive 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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session data class CreateSpaceState( @@ -39,8 +48,9 @@ data class CreateSpaceState( val topic: String = "", val step: Step = Step.ChooseType, val spaceType: SpaceType? = null, - val nameInlineError : String? = null, - val defaultRooms: List? = null + val nameInlineError: String? = null, + val defaultRooms: Map? = null, + val creationResult: Async = Uninitialized ) : MvRxState { enum class Step { @@ -59,8 +69,11 @@ sealed class CreateSpaceAction : VectorViewModelAction { data class SetRoomType(val type: SpaceType) : CreateSpaceAction() data class NameChanged(val name: String) : CreateSpaceAction() data class TopicChanged(val topic: String) : CreateSpaceAction() + data class SetAvatar(val uri: Uri?) : CreateSpaceAction() object OnBackPressed : CreateSpaceAction() object NextFromDetails : CreateSpaceAction() + object NextFromDefaultRooms : CreateSpaceAction() + data class DefaultRoomNameChanged(val index: Int, val name: String) : CreateSpaceAction() } sealed class CreateSpaceEvents : VectorViewEvents { @@ -68,12 +81,17 @@ sealed class CreateSpaceEvents : VectorViewEvents { object NavigateToChooseType : CreateSpaceEvents() object NavigateToAddRooms : CreateSpaceEvents() object Dismiss : CreateSpaceEvents() + data class FinishSuccess(val spaceId: String, val defaultRoomId: String?) : CreateSpaceEvents() + data class ShowModalError(val errorMessage: String) : CreateSpaceEvents() + object HideModalLoading : CreateSpaceEvents() } class CreateSpaceViewModel @AssistedInject constructor( @Assisted initialState: CreateSpaceState, private val session: Session, - private val stringProvider: StringProvider + private val stringProvider: StringProvider, + private val createSpaceViewModelTask: CreateSpaceViewModelTask, + private val errorFormatter: ErrorFormatter ) : VectorViewModel(initialState) { @AssistedFactory @@ -90,6 +108,12 @@ class CreateSpaceViewModel @AssistedInject constructor( } return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") } + + override fun initialState(viewModelContext: ViewModelContext): CreateSpaceState? { + return CreateSpaceState( + defaultRooms = mapOf(0 to viewModelContext.activity.getString(R.string.create_spaces_default_public_room_name)) + ) + } } override fun handle(action: CreateSpaceAction) { @@ -124,6 +148,21 @@ class CreateSpaceViewModel @AssistedInject constructor( CreateSpaceAction.NextFromDetails -> { handleNextFromDetails() } + CreateSpaceAction.NextFromDefaultRooms -> { + handleNextFromDefaultRooms() + } + is CreateSpaceAction.DefaultRoomNameChanged -> { + setState { + copy( + defaultRooms = (defaultRooms ?: emptyMap()).toMutableMap().apply { + this[action.index] = action.name + } + ) + } + } + is CreateSpaceAction.SetAvatar -> { + setState { copy(avatarUri = action.uri) } + } }.exhaustive } @@ -167,4 +206,53 @@ class CreateSpaceViewModel @AssistedInject constructor( _viewEvents.post(CreateSpaceEvents.NavigateToAddRooms) } } + + private fun handleNextFromDefaultRooms() = withState { state -> + val spaceName = state.name ?: return@withState + setState { + copy(creationResult = Loading()) + } + viewModelScope.launch(Dispatchers.IO) { + try { + val result = createSpaceViewModelTask.execute( + CreateSpaceTaskParams( + spaceName = spaceName, + spaceTopic = state.topic, + spaceAvatar = state.avatarUri, + isPublic = state.spaceType == SpaceType.Public, + defaultRooms = state.defaultRooms + ?.entries + ?.sortedBy { it.key } + ?.mapNotNull { it.value } ?: emptyList() + ) + ) + when (result) { + is CreateSpaceTaskResult.Success -> { + setState { + copy(creationResult = Success(result.spaceId)) + } + _viewEvents.post(CreateSpaceEvents.FinishSuccess(result.spaceId, result.childIds.firstOrNull())) + } + is CreateSpaceTaskResult.PartialSuccess -> { + // XXX what can we do here? + setState { + copy(creationResult = Success(result.spaceId)) + } + _viewEvents.post(CreateSpaceEvents.FinishSuccess(result.spaceId, result.childIds.firstOrNull())) + } + is CreateSpaceTaskResult.FailedToCreateSpace -> { + setState { + copy(creationResult = Fail(result.failure)) + } + _viewEvents.post(CreateSpaceEvents.ShowModalError(errorFormatter.toHumanReadable(result.failure))) + } + } + } catch (failure: Throwable) { + setState { + copy(creationResult = Fail(failure)) + } + _viewEvents.post(CreateSpaceEvents.ShowModalError(errorFormatter.toHumanReadable(failure))) + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt new file mode 100644 index 0000000000..a565e290f6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.create + +import android.net.Uri +import im.vector.app.core.platform.ViewModelTask +import im.vector.app.core.resources.StringProvider +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset +import org.matrix.android.sdk.internal.util.awaitCallback +import timber.log.Timber +import javax.inject.Inject + +sealed class CreateSpaceTaskResult { + + data class Success(val spaceId: String, val childIds: List) : CreateSpaceTaskResult() + + data class PartialSuccess(val spaceId: String, val childIds: List, val failedRooms: Map) : CreateSpaceTaskResult() + + class FailedToCreateSpace(val failure: Throwable) : CreateSpaceTaskResult() +} + +data class CreateSpaceTaskParams( + val spaceName: String, + val spaceTopic: String?, + val spaceAvatar: Uri? = null, + val isPublic: Boolean, + val defaultRooms: List = emptyList() +) + +class CreateSpaceViewModelTask @Inject constructor( + private val session: Session, + private val stringProvider: StringProvider +) : ViewModelTask { + + override suspend fun execute(params: CreateSpaceTaskParams): CreateSpaceTaskResult { + val spaceID = try { + session.spaceService().createSpace(params.spaceName, params.spaceTopic, params.spaceAvatar, params.isPublic) + } catch (failure: Throwable) { + return CreateSpaceTaskResult.FailedToCreateSpace(failure) + } + + val createdSpace = session.spaceService().getSpace(spaceID) + + val childErrors = mutableMapOf() + val childIds = mutableListOf() + if (params.isPublic) { + params.defaultRooms + .filter { it.isNotBlank() } + .forEach { roomName -> + try { + val roomId = try { + awaitCallback { + session.createRoom(CreateRoomParams().apply { + this.name = roomName + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + }, it) + } + } catch (timeout: CreateRoomFailure.CreatedWithTimeout) { + // we ignore that? + timeout.roomID + } + val via = session.sessionParams.homeServerHost?.let { listOf(it) } ?: emptyList() + createdSpace!!.addChildren(roomId, via, null, true) + childIds.add(roomId) + } catch (failure: Throwable) { + Timber.d("Failed to create child room in $spaceID") + childErrors[roomName] = failure + } + } + } + + return if (childErrors.isEmpty()) { + CreateSpaceTaskResult.Success(spaceID, childIds) + } else { + CreateSpaceTaskResult.PartialSuccess(spaceID, childIds, childErrors) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt index 05abcf95b0..27b57713ba 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt @@ -28,7 +28,6 @@ import javax.inject.Inject class SpaceDefaultRoomEpoxyController @Inject constructor( private val stringProvider: StringProvider, private val colorProvider: ColorProvider -// private val avatarRenderer: AvatarRenderer ) : TypedEpoxyController() { var listener: Listener? = null @@ -50,44 +49,41 @@ class SpaceDefaultRoomEpoxyController @Inject constructor( formEditTextItem { id("roomName1") enabled(true) - value(data?.name) - hint(stringProvider.getString(R.string.create_room_name_hint)) + value(data?.defaultRooms?.get(0)) + hint(stringProvider.getString(R.string.create_room_name_section)) showBottomSeparator(false) -// errorMessage(data?.nameInlineError) onTextChange { text -> -// listener?.onNameChange(text) + listener?.onNameChange(0, text) } } formEditTextItem { id("roomName2") enabled(true) -// value(data?.name) - hint(stringProvider.getString(R.string.create_room_name_hint)) + value(data?.defaultRooms?.get(1)) + hint(stringProvider.getString(R.string.create_room_name_section)) showBottomSeparator(false) -// errorMessage(data?.nameInlineError) onTextChange { text -> -// listener?.onNameChange(text) + listener?.onNameChange(1, text) } } formEditTextItem { id("roomName3") enabled(true) -// value(data?.name) - hint(stringProvider.getString(R.string.create_room_name_hint)) + value(data?.defaultRooms?.get(2)) + hint(stringProvider.getString(R.string.create_room_name_section)) showBottomSeparator(false) -// errorMessage(data?.nameInlineError) onTextChange { text -> -// listener?.onNameChange(text) + listener?.onNameChange(2, text) } } } interface Listener { -// fun onAvatarDelete() + // fun onAvatarDelete() // fun onAvatarChange() -// fun onNameChange(newName: String) + fun onNameChange(index: Int, newName: String) // fun onTopicChange(newTopic: String) } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt b/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt index 552a98ded2..066c329c34 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt @@ -18,6 +18,7 @@ package im.vector.app.features.spaces.create import android.content.Context import android.graphics.drawable.Drawable +import android.os.Build import android.util.AttributeSet import android.util.TypedValue import androidx.appcompat.content.res.AppCompatResources.getDrawable @@ -85,7 +86,7 @@ class WizardButtonView @JvmOverloads constructor(context: Context, attrs: Attrib val outValue = TypedValue() context.theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true) - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { this.foreground = getDrawable(context, outValue.resourceId) } diff --git a/vector/src/main/res/values/ids.xml b/vector/src/main/res/values/ids.xml new file mode 100644 index 0000000000..099d6f4279 --- /dev/null +++ b/vector/src/main/res/values/ids.xml @@ -0,0 +1,3 @@ + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index b2bfe76f57..fb578f3802 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3275,6 +3275,8 @@ Give it a name to continue. What are some discussions you want to have in Runner’s World? We’ll create rooms for them, and auto-join everyone. You can add more later too. + General + Creating Space… From a901e1d1791c9e33019ef966d8c395eddc8440e2 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 25 Feb 2021 18:22:16 +0100 Subject: [PATCH 18/97] Post Creation Share flow --- .../im/vector/app/core/di/ScreenComponent.kt | 2 + .../grouplist/HomeSpaceSummaryItem.kt | 3 + .../vector/app/features/home/HomeActivity.kt | 8 +- .../home/room/detail/RoomDetailFragment.kt | 164 ++++++++++-------- .../features/navigation/DefaultNavigator.kt | 11 +- .../app/features/navigation/Navigator.kt | 4 +- .../features/permalink/PermalinkHandler.kt | 53 +++--- .../features/spaces/ShareSpaceBottomSheet.kt | 107 ++++++++++++ .../app/features/spaces/SpaceListFragment.kt | 4 + .../features/spaces/SpaceSummaryController.kt | 3 + .../app/features/spaces/SpaceSummaryItem.kt | 13 ++ .../features/spaces/SpacesListViewModel.kt | 39 ++++- .../spaces/create/WizardButtonView.kt | 31 ++-- vector/src/main/res/drawable/ic_mail.xml | 21 +++ .../src/main/res/drawable/ic_share_link.xml | 12 ++ .../res/layout/bottom_sheet_space_invite.xml | 79 +++++++++ vector/src/main/res/layout/item_space.xml | 17 +- .../res/layout/view_space_type_button.xml | 6 +- vector/src/main/res/values/strings.xml | 8 + 19 files changed, 454 insertions(+), 131 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt create mode 100644 vector/src/main/res/drawable/ic_mail.xml create mode 100644 vector/src/main/res/drawable/ic_share_link.xml create mode 100644 vector/src/main/res/layout/bottom_sheet_space_invite.xml diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index 3633bb610a..f9ffc9c612 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -77,6 +77,7 @@ import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheet import im.vector.app.features.share.IncomingShareActivity import im.vector.app.features.signout.soft.SoftLogoutActivity +import im.vector.app.features.spaces.ShareSpaceBottomSheet import im.vector.app.features.spaces.SpaceCreationActivity import im.vector.app.features.terms.ReviewTermsActivity import im.vector.app.features.ui.UiStateRepository @@ -175,6 +176,7 @@ interface ScreenComponent { fun inject(bottomSheet: CallControlsBottomSheet) fun inject(bottomSheet: SignOutBottomSheetDialogFragment) fun inject(bottomSheet: MatrixToBottomSheet) + fun inject(bottomSheet: ShareSpaceBottomSheet) /* ========================================================================================== * Others 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 index c3f459f49e..f4c95215b8 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt @@ -22,6 +22,7 @@ import android.util.TypedValue import android.widget.ImageView import android.widget.TextView import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -48,12 +49,14 @@ abstract class HomeSpaceSummaryItem : VectorEpoxyModel(R.id.groupAvatarImageView) val groupNameView by bind(R.id.groupNameView) val rootView by bind(R.id.itemGroupLayout) + val leaveView by bind(R.id.groupTmpLeave) } fun dpToPx(resources: Resources, dp: Int): Int { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 40d0cdd622..db7bb8fa16 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -111,14 +111,10 @@ class HomeActivity : val defaultRoomsId = activityResult.data?.extras?.getString(SpaceCreationActivity.RESULT_DATA_DEFAULT_ROOM_ID) views.drawerLayout.closeDrawer(GravityCompat.START) - // Here we want to change current space to the newly created one, and then immediatly open the default room + // Here we want to change current space to the newly created one, and then immediately open the default room if (spaceId != null) { - navigator.switchToSpace(this, spaceId, defaultRoomsId) + navigator.switchToSpace(this, spaceId, defaultRoomsId, true) } - - // Also we should show the share space bottomsheet - } else { - // viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index b7e2e189d3..a585d7609a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -91,7 +91,9 @@ import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.CurrentCallsView +import im.vector.app.core.ui.views.JumpToReadMarkerView import im.vector.app.core.ui.views.KnownCallsViewHolder import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.FailedMessagesWarningView @@ -163,6 +165,7 @@ import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.share.SharedData +import im.vector.app.features.spaces.ShareSpaceBottomSheet import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgs @@ -210,7 +213,8 @@ import javax.inject.Inject data class RoomDetailArgs( val roomId: String, val eventId: String? = null, - val sharedData: SharedData? = null + val sharedData: SharedData? = null, + val openShareSpaceForId: String? = null ) : Parcelable class RoomDetailFragment @Inject constructor( @@ -293,7 +297,7 @@ class RoomDetailFragment @Inject constructor( private lateinit var attachmentsHelper: AttachmentsHelper private lateinit var keyboardStateUtils: KeyboardStateUtils - private lateinit var callActionsHandler : StartCallActionsHandler + private lateinit var callActionsHandler: StartCallActionsHandler private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView @@ -358,9 +362,9 @@ class RoomDetailFragment @Inject constructor( } when (mode) { is SendMode.REGULAR -> renderRegularMode(mode.text) - is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text) - is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text) - is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text) + is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text) + is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text) + is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text) } } @@ -384,29 +388,30 @@ class RoomDetailFragment @Inject constructor( RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager() is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it) is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog() - is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager() - is RoomDetailViewEvents.OpenFile -> startOpenFileIntent(it) - RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked() - is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message) - is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo) - RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView() - RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() - is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) - is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) - RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId) - RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show() - RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings() - is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item -> + is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager() + is RoomDetailViewEvents.OpenFile -> startOpenFileIntent(it) + RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked() + is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message) + is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo) + RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView() + RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() + is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) + is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) + RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId) + RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show() + RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings() + is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item -> navigator.openBigImageViewer(requireActivity(), it.view, item) } - is RoomDetailViewEvents.StartChatEffect -> handleChatEffect(it.type) - RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() - is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) + is RoomDetailViewEvents.StartChatEffect -> handleChatEffect(it.type) + RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() + is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) }.exhaustive } if (savedInstanceState == null) { handleShareData() + handleSpaceShare() } } @@ -419,7 +424,7 @@ class RoomDetailFragment @Inject constructor( startActivity(intent) } - private fun handleChatEffect(chatEffect: ChatEffect) { + private fun handleChatEffect(chatEffect: ChatEffect) { when (chatEffect) { ChatEffect.CONFETTI -> { views.viewKonfetti.isVisible = true @@ -434,7 +439,7 @@ class RoomDetailFragment @Inject constructor( .setPosition(-50f, views.viewKonfetti.width + 50f, -50f, -50f) .streamFor(150, 3000L) } - ChatEffect.SNOW -> { + ChatEffect.SNOW -> { views.viewSnowFall.isVisible = true views.viewSnowFall.restartFalling() } @@ -627,17 +632,26 @@ class RoomDetailFragment @Inject constructor( private fun handleShareData() { when (val sharedData = roomDetailArgs.sharedData) { - is SharedData.Text -> { + is SharedData.Text -> { roomDetailViewModel.handle(RoomDetailAction.EnterRegularMode(sharedData.text, fromSharing = true)) } is SharedData.Attachments -> { // open share edition onContentAttachmentsReady(sharedData.attachmentData) } - null -> Timber.v("No share data to process") + null -> Timber.v("No share data to process") }.exhaustive } + private fun handleSpaceShare() { + roomDetailArgs.openShareSpaceForId?.let { spaceId -> + ShareSpaceBottomSheet.show(childFragmentManager, spaceId) + view?.post { + handleChatEffect(ChatEffect.CONFETTI) + } + } + } + override fun onDestroyView() { timelineEventController.callback = null timelineEventController.removeModelBuildListener(modelBuildListener) @@ -760,8 +774,8 @@ class RoomDetailFragment @Inject constructor( withState(roomDetailViewModel) { state -> // Set the visual state of the call buttons (voice/video) to enabled/disabled according to user permissions val callButtonsEnabled = when (state.asyncRoomSummary.invoke()?.joinedMembersCount) { - 1 -> false - 2 -> state.isAllowedToStartWebRTCCall + 1 -> false + 2 -> state.isAllowedToStartWebRTCCall else -> state.isAllowedToManageWidgets } setOf(R.id.voice_call, R.id.video_call).forEach { @@ -791,7 +805,7 @@ class RoomDetailFragment @Inject constructor( override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { - R.id.invite -> { + R.id.invite -> { navigator.openInviteUsersToRoom(requireActivity(), roomDetailArgs.roomId) true } @@ -807,19 +821,19 @@ class RoomDetailFragment @Inject constructor( callActionsHandler.onVoiceCallClicked() true } - R.id.video_call -> { + R.id.video_call -> { callActionsHandler.onVideoCallClicked() true } - R.id.hangup_call -> { + R.id.hangup_call -> { roomDetailViewModel.handle(RoomDetailAction.EndCall) true } - R.id.search -> { + R.id.search -> { handleSearchAction() true } - R.id.dev_tools -> { + R.id.dev_tools -> { navigator.openDevTools(requireContext(), roomDetailArgs.roomId) true } @@ -921,9 +935,9 @@ class RoomDetailFragment @Inject constructor( when (roomDetailPendingAction) { is RoomDetailPendingAction.JumpToReadReceipt -> roomDetailViewModel.handle(RoomDetailAction.JumpToReadReceipt(roomDetailPendingAction.userId)) - is RoomDetailPendingAction.MentionUser -> + is RoomDetailPendingAction.MentionUser -> insertUserDisplayNameInTextEditor(roomDetailPendingAction.userId) - is RoomDetailPendingAction.OpenOrCreateDm -> + is RoomDetailPendingAction.OpenOrCreateDm -> roomDetailViewModel.handle(RoomDetailAction.OpenOrCreateDm(roomDetailPendingAction.userId)) }.exhaustive } @@ -1078,9 +1092,9 @@ class RoomDetailFragment @Inject constructor( withState(roomDetailViewModel) { val showJumpToUnreadBanner = when (it.unreadState) { UnreadState.Unknown, - UnreadState.HasNoUnread -> false + UnreadState.HasNoUnread -> false is UnreadState.ReadMarkerNotLoaded -> true - is UnreadState.HasUnread -> { + is UnreadState.HasUnread -> { if (it.canShowJumpToReadMarker) { val lastVisibleItem = layoutManager.findLastVisibleItemPosition() val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() @@ -1268,7 +1282,7 @@ class RoomDetailFragment @Inject constructor( navigator.openRoom(vectorBaseActivity, async()) vectorBaseActivity.finish() } - is Fail -> { + is Fail -> { vectorBaseActivity.hideWaitingView() vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error)) } @@ -1277,19 +1291,19 @@ class RoomDetailFragment @Inject constructor( private fun renderSendMessageResult(sendMessageResult: RoomDetailViewEvents.SendMessageResult) { when (sendMessageResult) { - is RoomDetailViewEvents.SlashCommandHandled -> { + is RoomDetailViewEvents.SlashCommandHandled -> { sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } } - is RoomDetailViewEvents.SlashCommandError -> { + is RoomDetailViewEvents.SlashCommandError -> { displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) } - is RoomDetailViewEvents.SlashCommandUnknown -> { + is RoomDetailViewEvents.SlashCommandUnknown -> { displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) } - is RoomDetailViewEvents.SlashCommandResultOk -> { + is RoomDetailViewEvents.SlashCommandResultOk -> { updateComposerText("") } - is RoomDetailViewEvents.SlashCommandResultError -> { + is RoomDetailViewEvents.SlashCommandResultError -> { displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable)) } is RoomDetailViewEvents.SlashCommandNotImplemented -> { @@ -1311,7 +1325,7 @@ class RoomDetailFragment @Inject constructor( private fun displayE2eError(withHeldCode: WithHeldCode?) { val msgId = when (withHeldCode) { WithHeldCode.BLACKLISTED -> R.string.crypto_error_withheld_blacklisted - WithHeldCode.UNVERIFIED -> R.string.crypto_error_withheld_unverified + WithHeldCode.UNVERIFIED -> R.string.crypto_error_withheld_unverified WithHeldCode.UNAUTHORISED, WithHeldCode.UNAVAILABLE -> R.string.crypto_error_withheld_generic else -> R.string.notice_crypto_unable_to_decrypt_friendly_desc @@ -1362,7 +1376,7 @@ class RoomDetailFragment @Inject constructor( private fun displayRoomDetailActionSuccess(result: RoomDetailViewEvents.ActionSuccess) { when (val data = result.action) { - is RoomDetailAction.ReportContent -> { + is RoomDetailAction.ReportContent -> { when { data.spam -> { AlertDialog.Builder(requireActivity()) @@ -1399,7 +1413,7 @@ class RoomDetailFragment @Inject constructor( } } } - is RoomDetailAction.RequestVerification -> { + is RoomDetailAction.RequestVerification -> { Timber.v("## SAS RequestVerification action") VerificationBottomSheet.withArgs( roomDetailArgs.roomId, @@ -1414,7 +1428,7 @@ class RoomDetailFragment @Inject constructor( data.transactionId ).show(parentFragmentManager, "REQ") } - is RoomDetailAction.ResumeVerification -> { + is RoomDetailAction.ResumeVerification -> { val otherUserId = data.otherUserId ?: return VerificationBottomSheet().apply { arguments = Bundle().apply { @@ -1557,11 +1571,11 @@ class RoomDetailFragment @Inject constructor( is MessageVerificationRequestContent -> { roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null)) } - is MessageWithAttachmentContent -> { + is MessageWithAttachmentContent -> { val action = RoomDetailAction.DownloadOrOpen(informationData.eventId, informationData.senderId, messageContent) roomDetailViewModel.handle(action) } - is EncryptedEventContent -> { + is EncryptedEventContent -> { roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId)) } } @@ -1724,75 +1738,75 @@ class RoomDetailFragment @Inject constructor( private fun handleActions(action: EventSharedAction) { when (action) { - is EventSharedAction.OpenUserProfile -> { + is EventSharedAction.OpenUserProfile -> { openRoomMemberProfile(action.userId) } - is EventSharedAction.AddReaction -> { + is EventSharedAction.AddReaction -> { emojiActivityResultLauncher.launch(EmojiReactionPickerActivity.intent(requireContext(), action.eventId)) } - is EventSharedAction.ViewReactions -> { + is EventSharedAction.ViewReactions -> { ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } - is EventSharedAction.Copy -> { + is EventSharedAction.Copy -> { // I need info about the current selected message :/ copyToClipboard(requireContext(), action.content, false) showSnackWithMessage(getString(R.string.copied_to_clipboard)) } - is EventSharedAction.Redact -> { + is EventSharedAction.Redact -> { promptConfirmationToRedactEvent(action) } - is EventSharedAction.Share -> { + is EventSharedAction.Share -> { onShareActionClicked(action) } - is EventSharedAction.Save -> { + is EventSharedAction.Save -> { onSaveActionClicked(action) } - is EventSharedAction.ViewEditHistory -> { + is EventSharedAction.ViewEditHistory -> { onEditedDecorationClicked(action.messageInformationData) } - is EventSharedAction.ViewSource -> { + is EventSharedAction.ViewSource -> { JSonViewerDialog.newInstance( action.content, -1, createJSonViewerStyleProvider(colorProvider) ).show(childFragmentManager, "JSON_VIEWER") } - is EventSharedAction.ViewDecryptedSource -> { + is EventSharedAction.ViewDecryptedSource -> { JSonViewerDialog.newInstance( action.content, -1, createJSonViewerStyleProvider(colorProvider) ).show(childFragmentManager, "JSON_VIEWER") } - is EventSharedAction.QuickReact -> { + is EventSharedAction.QuickReact -> { // eventId,ClickedOn,Add roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } - is EventSharedAction.Edit -> { + is EventSharedAction.Edit -> { roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) } - is EventSharedAction.Quote -> { + is EventSharedAction.Quote -> { roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString())) } - is EventSharedAction.Reply -> { + is EventSharedAction.Reply -> { roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString())) } - is EventSharedAction.CopyPermalink -> { + is EventSharedAction.CopyPermalink -> { val permalink = session.permalinkService().createPermalink(roomDetailArgs.roomId, action.eventId) copyToClipboard(requireContext(), permalink, false) showSnackWithMessage(getString(R.string.copied_to_clipboard)) } - is EventSharedAction.Resend -> { + is EventSharedAction.Resend -> { roomDetailViewModel.handle(RoomDetailAction.ResendMessage(action.eventId)) } - is EventSharedAction.Remove -> { + is EventSharedAction.Remove -> { roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId)) } is EventSharedAction.Cancel -> { handleCancelSend(action) } - is EventSharedAction.ReportContentSpam -> { + is EventSharedAction.ReportContentSpam -> { roomDetailViewModel.handle(RoomDetailAction.ReportContent( action.eventId, action.senderId, "This message is spam", spam = true)) } @@ -1800,22 +1814,22 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.ReportContent( action.eventId, action.senderId, "This message is inappropriate", inappropriate = true)) } - is EventSharedAction.ReportContentCustom -> { + is EventSharedAction.ReportContentCustom -> { promptReasonToReportContent(action) } - is EventSharedAction.IgnoreUser -> { + is EventSharedAction.IgnoreUser -> { action.senderId?.let { askConfirmationToIgnoreUser(it) } } - is EventSharedAction.OnUrlClicked -> { + is EventSharedAction.OnUrlClicked -> { onUrlClicked(action.url, action.title) } - is EventSharedAction.OnUrlLongClicked -> { + is EventSharedAction.OnUrlLongClicked -> { onUrlLongClicked(action.url) } - is EventSharedAction.ReRequestKey -> { + is EventSharedAction.ReRequestKey -> { roomDetailViewModel.handle(RoomDetailAction.ReRequestKeys(action.eventId)) } - is EventSharedAction.UseKeyBackup -> { + is EventSharedAction.UseKeyBackup -> { context?.let { startActivity(KeysBackupRestoreActivity.intent(it)) } @@ -1955,10 +1969,10 @@ class RoomDetailFragment @Inject constructor( private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { when (type) { - AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(requireContext(), attachmentPhotoActivityResultLauncher) - AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) + AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(requireContext(), attachmentPhotoActivityResultLauncher) + AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentImageActivityResultLauncher) - AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(attachmentAudioActivityResultLauncher) + AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(attachmentAudioActivityResultLauncher) AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher) AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment) }.exhaustive diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index d6eeade97e..80cb82098b 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -70,6 +70,7 @@ import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.share.SharedData +import im.vector.app.features.spaces.SpacePreviewActivity import im.vector.app.features.terms.ReviewTermsActivity import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgsBuilder @@ -102,7 +103,7 @@ class DefaultNavigator @Inject constructor( startActivity(context, intent, buildTask) } - override fun switchToSpace(context: Context, spaceId: String, roomId: String?) { + override fun switchToSpace(context: Context, spaceId: String, roomId: String?, openShareSheet: Boolean) { if (sessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(spaceId) == null) { fatalError("Trying to open an unknown space $spaceId", vectorPreferences.failFast()) return @@ -115,10 +116,16 @@ class DefaultNavigator @Inject constructor( Timber.d("## Nav: Failed to switch to space $spaceId") } if (roomId != null) { - openRoom(context, roomId) + val args = RoomDetailArgs(roomId, eventId = null, openShareSpaceForId = spaceId.takeIf { openShareSheet }) + val intent = RoomDetailActivity.newIntent(context, args) + startActivity(context, intent, false) } } + override fun openSpacePreview(context: Context, spaceId: String) { + startActivity(context, SpacePreviewActivity.newIntent(context, spaceId), false) + } + override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) { val session = sessionHolder.getSafeActiveSession() ?: return val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index fcd35be162..225a2b89ab 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -38,7 +38,9 @@ interface Navigator { fun openRoom(context: Context, roomId: String, eventId: String? = null, buildTask: Boolean = false) - fun switchToSpace(context: Context, spaceId: String, roomId: String?) + fun switchToSpace(context: Context, spaceId: String, roomId: String?, openShareSheet: Boolean) + + fun openSpacePreview(context: Context, spaceId: String) fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt index ae6d630c75..d43337fab0 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.rx.rx @@ -147,33 +148,43 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti val membership = roomSummary?.membership val eventId = permalinkData.eventId val roomAlias = permalinkData.getRoomAliasOrNull() + val isSpace = roomSummary?.roomType == RoomType.SPACE return when { membership == Membership.BAN -> context.toast(R.string.error_opening_banned_room) membership?.isActive().orFalse() -> { - navigator.openRoom(context, roomId, eventId, buildTask) + if (isSpace) { +// navigator.switchToSpace(context, roomId, null) + navigator.openSpacePreview(context, roomId) + } else { + navigator.openRoom(context, roomId, eventId, buildTask) + } } else -> { - if (roomSummary == null) { - // we don't know this room, try to peek - val roomPreviewData = RoomPreviewData( - roomId = roomId, - roomAlias = roomAlias, - peekFromServer = true, - buildTask = buildTask, - homeServers = permalinkData.viaParameters - ) - navigator.openRoomPreview(context, roomPreviewData) + if (isSpace) { + navigator.openSpacePreview(context, roomId) } else { - val roomPreviewData = RoomPreviewData( - roomId = roomId, - eventId = eventId, - roomAlias = roomAlias ?: roomSummary.canonicalAlias, - roomName = roomSummary.displayName, - avatarUrl = roomSummary.avatarUrl, - buildTask = buildTask, - homeServers = permalinkData.viaParameters - ) - navigator.openRoomPreview(context, roomPreviewData) + if (roomSummary == null) { + // we don't know this room, try to peek + val roomPreviewData = RoomPreviewData( + roomId = roomId, + roomAlias = roomAlias, + peekFromServer = true, + buildTask = buildTask, + homeServers = permalinkData.viaParameters + ) + navigator.openRoomPreview(context, roomPreviewData) + } else { + val roomPreviewData = RoomPreviewData( + roomId = roomId, + eventId = eventId, + roomAlias = roomAlias ?: roomSummary.canonicalAlias, + roomName = roomSummary.displayName, + avatarUrl = roomSummary.avatarUrl, + buildTask = buildTask, + homeServers = permalinkData.viaParameters + ) + navigator.openRoomPreview(context, roomPreviewData) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt new file mode 100644 index 0000000000..d3fb225083 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import im.vector.app.R +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.core.utils.startSharePlainTextIntent +import im.vector.app.databinding.BottomSheetSpaceInviteBinding +import im.vector.app.features.invite.InviteUsersToRoomActivity +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment() { + + @Parcelize + data class Args( + val spaceId: String + ) : Parcelable + + override val showExpanded = true + + @Inject + lateinit var activeSessionHolder: ActiveSessionHolder + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSpaceInviteBinding { + return BottomSheetSpaceInviteBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Not going for full view model for now, as it may change + + val args: Args = arguments?.getParcelable(EXTRA_ARGS) + ?: return Unit.also { dismiss() } + val summary = activeSessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(args.spaceId)?.spaceSummary() + + val spaceName = summary?.roomSummary?.name + views.descriptionText.text = getString(R.string.invite_people_to_your_space_desc, spaceName) + + views.inviteByMailButton.debouncedClicks { + } + + views.inviteByMxidButton.debouncedClicks { + val intent = InviteUsersToRoomActivity.getIntent(requireContext(), args.spaceId) + startActivity(intent) + } + + views.inviteByLinkButton.debouncedClicks { + activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createRoomPermalink(args.spaceId)?.let { permalink -> + startSharePlainTextIntent( + fragment = this, + activityResultLauncher = null, + chooserTitle = getString(R.string.share_by_text), + text = getString(R.string.share_space_link_message, spaceName, permalink), + extraTitle = getString(R.string.share_space_link_message, spaceName, permalink) + ) + } + } + + views.skipButton.debouncedClicks { + dismiss() + } + } + + companion object { + + const val EXTRA_ARGS = "EXTRA_ARGS" + + fun show(fragmentManager: FragmentManager, spaceId: String): ShareSpaceBottomSheet { + return ShareSpaceBottomSheet().apply { + isCancelable = true + arguments = Bundle().apply { + this.putParcelable(EXTRA_ARGS, ShareSpaceBottomSheet.Args(spaceId = spaceId)) + } + }.also { + it.show(fragmentManager, ShareSpaceBottomSheet::class.java.name) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt index bc1af38473..f2ef3b84a0 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt @@ -80,6 +80,10 @@ class SpaceListFragment @Inject constructor( viewModel.handle(SpaceListAction.SelectSpace(spaceSummary)) } + override fun onLeaveSpace(spaceSummary: SpaceSummary) { + viewModel.handle(SpaceListAction.LeaveSpace(spaceSummary)) + } + override fun onAddSpaceSelected() { viewModel.handle(SpaceListAction.AddSpace) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt index d1bc3c6e1c..0402beb428 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt @@ -71,6 +71,7 @@ class SpaceSummaryController @Inject constructor( matrixItem(it.toMatrixItem()) selected(false) listener { callback?.onSpaceSelected(it) } +// lea { callback?.onSpaceSelected(it) } } } genericFooterItem { @@ -101,6 +102,7 @@ class SpaceSummaryController @Inject constructor( id(groupSummary.spaceId) matrixItem(groupSummary.toMatrixItem()) selected(isSelected) + onLeave { callback?.onLeaveSpace(groupSummary) } listener { callback?.onSpaceSelected(groupSummary) } } } @@ -118,6 +120,7 @@ class SpaceSummaryController @Inject constructor( interface Callback { fun onSpaceSelected(spaceSummary: SpaceSummary) + fun onLeaveSpace(spaceSummary: SpaceSummary) fun onAddSpaceSelected() } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt index bf3a47461f..3f1971d9dc 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt @@ -18,12 +18,14 @@ package im.vector.app.features.spaces import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.platform.CheckableConstraintLayout +import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem @@ -34,12 +36,22 @@ abstract class SpaceSummaryItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var listener: (() -> Unit)? = null + @EpoxyAttribute var onLeave: (() -> Unit)? = null override fun bind(holder: Holder) { super.bind(holder) holder.rootView.setOnClickListener { listener?.invoke() } holder.groupNameView.text = matrixItem.displayName holder.rootView.isChecked = selected + if (onLeave != null) { + holder.leaveView.setOnClickListener( + DebouncedClickListener({ _ -> + onLeave?.invoke() + }) + ) + } else { + holder.leaveView.isVisible = false + } avatarRenderer.renderSpace(matrixItem, holder.avatarImageView) } @@ -52,5 +64,6 @@ abstract class SpaceSummaryItem : VectorEpoxyModel() { val avatarImageView by bind(R.id.groupAvatarImageView) val groupNameView by bind(R.id.groupNameView) val rootView by bind(R.id.itemGroupLayout) + val leaveView by bind(R.id.groupTmpLeave) } } 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 index 4ec6e7d4ad..28bc358e36 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.spaces +import androidx.lifecycle.viewModelScope import arrow.core.Option import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext @@ -34,18 +35,22 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.grouplist.SelectedSpaceDataSource import io.reactivex.Observable import io.reactivex.functions.BiFunction +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.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.space.SpaceSummary +import org.matrix.android.sdk.internal.util.awaitCallback 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() + data class LeaveSpace(val spaceSummary: SpaceSummary) : SpaceListAction() object AddSpace : SpaceListAction() } @@ -88,13 +93,18 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp init { observeSpaceSummaries() observeSelectionState() - selectedSpaceDataSource.observe().execute { - if (this.selectedSpace != it.invoke()?.orNull()) { - copy( - selectedSpace = it.invoke()?.orNull() - ) - } else this - } + selectedSpaceDataSource + .observe() + .subscribe { + if (currentGroupId != it.orNull()?.spaceId) { + setState { + copy( + selectedSpace = it.orNull() + ) + } + } + } + .disposeOnClear() } private fun observeSelectionState() { @@ -119,11 +129,12 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp override fun handle(action: SpaceListAction) { when (action) { is SpaceListAction.SelectSpace -> handleSelectSpace(action) - else -> handleAddSpace() + is SpaceListAction.LeaveSpace -> handleLeaveSpace(action) + SpaceListAction.AddSpace -> handleAddSpace() } } - // PRIVATE METHODS ***************************************************************************** +// PRIVATE METHODS ***************************************************************************** private fun handleSelectSpace(action: SpaceListAction.SelectSpace) = withState { state -> // get uptodate version of the space @@ -146,6 +157,16 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp } } + private fun handleLeaveSpace(action: SpaceListAction.LeaveSpace) { + viewModelScope.launch { + awaitCallback { + tryOrNull("Failed to leave space ${action.spaceSummary.spaceId}") { + session.spaceService().getSpace(action.spaceSummary.spaceId)?.asRoom()?.leave(null, it) + } + } + } + } + private fun handleAddSpace() { _viewEvents.post(SpaceListViewEvents.AddSpace) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt b/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt index 066c329c34..a6b9b1978a 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt @@ -17,6 +17,7 @@ package im.vector.app.features.spaces.create import android.content.Context +import android.content.res.ColorStateList import android.graphics.drawable.Drawable import android.os.Build import android.util.AttributeSet @@ -25,6 +26,7 @@ import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.withStyledAttributes import im.vector.app.R +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.databinding.ViewSpaceTypeButtonBinding class WizardButtonView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) @@ -36,7 +38,7 @@ class WizardButtonView @JvmOverloads constructor(context: Context, attrs: Attrib set(value) { if (value != title) { field = value - views.title.text = value + views.title.setTextOrHide(value) } } @@ -44,7 +46,7 @@ class WizardButtonView @JvmOverloads constructor(context: Context, attrs: Attrib set(value) { if (value != subTitle) { field = value - views.subTitle.text = value + views.subTitle.setTextOrHide(value) } } @@ -56,15 +58,13 @@ class WizardButtonView @JvmOverloads constructor(context: Context, attrs: Attrib } } -// private var tint: Int? = null -// set(value) { -// field = value -// if (value != null) { -// views.buttonImageView.imageTintList = ColorStateList.valueOf(value) -// } else { -// views.buttonImageView.clearColorFilter() -// } -// } + private var tint: Int? = null + set(value) { + field = value + if (value != null) { + views.buttonImageView.imageTintList = ColorStateList.valueOf(value) + } + } // var action: (() -> Unit)? = null @@ -72,16 +72,19 @@ class WizardButtonView @JvmOverloads constructor(context: Context, attrs: Attrib inflate(context, R.layout.view_space_type_button, this) views = ViewSpaceTypeButtonBinding.bind(this) + views.subTitle.setTextOrHide(null) + if (isInEditMode) { title = "Title" subTitle = "This is doing something" } context.withStyledAttributes(attrs, R.styleable.WizardButtonView) { - title = getString(R.styleable.WizardButtonView_title) ?: "" - subTitle = getString(R.styleable.WizardButtonView_subTitle) ?: "" + title = getString(R.styleable.WizardButtonView_title) + subTitle = getString(R.styleable.WizardButtonView_subTitle) icon = getDrawable(R.styleable.WizardButtonView_icon) -// tint = getColor(R.styleable.WizardButtonView_iconTint, ThemeUtils.getColor(context, R.attr.riotx_text_primary)) + tint = getColor(R.styleable.WizardButtonView_iconTint, -1) + .takeIf { it != -1 } } val outValue = TypedValue() diff --git a/vector/src/main/res/drawable/ic_mail.xml b/vector/src/main/res/drawable/ic_mail.xml new file mode 100644 index 0000000000..80d25166b0 --- /dev/null +++ b/vector/src/main/res/drawable/ic_mail.xml @@ -0,0 +1,21 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_share_link.xml b/vector/src/main/res/drawable/ic_share_link.xml new file mode 100644 index 0000000000..d4eceb3b9a --- /dev/null +++ b/vector/src/main/res/drawable/ic_share_link.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/layout/bottom_sheet_space_invite.xml b/vector/src/main/res/layout/bottom_sheet_space_invite.xml new file mode 100644 index 0000000000..c8d29b2f7d --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_space_invite.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_space.xml b/vector/src/main/res/layout/item_space.xml index 9cd07f3215..f52ef31ead 100644 --- a/vector/src/main/res/layout/item_space.xml +++ b/vector/src/main/res/layout/item_space.xml @@ -35,11 +35,26 @@ android:textSize="15sp" android:textStyle="bold" app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator" - app:layout_constraintEnd_toStartOf="@+id/groupAvatarChevron" + app:layout_constraintEnd_toStartOf="@+id/groupTmpLeave" app:layout_constraintStart_toEndOf="@+id/groupAvatarImageView" app:layout_constraintTop_toTopOf="parent" tools:text="@tools:sample/lorem/random" /> + + @@ -53,7 +54,8 @@ app:layout_constraintStart_toEndOf="@id/buttonImageView" app:layout_constraintTop_toBottomOf="@id/title" app:layout_goneMarginBottom="8dp" - tools:text="Open to anyone, best for communities" /> + tools:text="Open to anyone, best for communities" + tools:visibility="visible" /> We’ll create rooms for them, and auto-join everyone. You can add more later too. General Creating Space… + Invite people to your space + It’s just you at the moment. %s will be even better with others. + Invite by email + Invite by username + Share link + + Join my space %1$s %2$s + Skip for now From 2952dca3a35e2309f2f10ad171e9cd25f4d0ce35 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 25 Feb 2021 18:50:49 +0100 Subject: [PATCH 19/97] Default Rooms Handling --- .../java/im/vector/app/features/form/FormEditTextItem.kt | 7 +++++++ .../app/features/spaces/create/CreateSpaceViewModel.kt | 5 ++++- .../spaces/create/SpaceDefaultRoomEpoxyController.kt | 4 ++++ vector/src/main/res/values/strings.xml | 1 + 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt index a1bd2bd1a3..42ce6d68a8 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt @@ -57,6 +57,9 @@ abstract class FormEditTextItem : VectorEpoxyModel() { @EpoxyAttribute var imeOptions: Int? = null + @EpoxyAttribute + var endIconMode: Int? = null + @EpoxyAttribute var onTextChange: ((String) -> Unit)? = null @@ -72,6 +75,10 @@ abstract class FormEditTextItem : VectorEpoxyModel() { holder.textInputLayout.hint = hint holder.textInputLayout.error = errorMessage + endIconMode?.let { mode -> + holder.textInputLayout.endIconMode = mode + } + // Update only if text is different and value is not null holder.textInputEditText.setTextSafe(value) holder.textInputEditText.isEnabled = enabled diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt index cd2680ef5f..9988bbe003 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt @@ -111,7 +111,10 @@ class CreateSpaceViewModel @AssistedInject constructor( override fun initialState(viewModelContext: ViewModelContext): CreateSpaceState? { return CreateSpaceState( - defaultRooms = mapOf(0 to viewModelContext.activity.getString(R.string.create_spaces_default_public_room_name)) + defaultRooms = mapOf( + 0 to viewModelContext.activity.getString(R.string.create_spaces_default_public_room_name), + 1 to viewModelContext.activity.getString(R.string.create_spaces_default_public_random_room_name) + ) ) } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt index 27b57713ba..800de01b62 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt @@ -17,6 +17,7 @@ package im.vector.app.features.spaces.create import com.airbnb.epoxy.TypedEpoxyController +import com.google.android.material.textfield.TextInputLayout import im.vector.app.R import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider @@ -51,6 +52,7 @@ class SpaceDefaultRoomEpoxyController @Inject constructor( enabled(true) value(data?.defaultRooms?.get(0)) hint(stringProvider.getString(R.string.create_room_name_section)) + endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT) showBottomSeparator(false) onTextChange { text -> listener?.onNameChange(0, text) @@ -62,6 +64,7 @@ class SpaceDefaultRoomEpoxyController @Inject constructor( enabled(true) value(data?.defaultRooms?.get(1)) hint(stringProvider.getString(R.string.create_room_name_section)) + endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT) showBottomSeparator(false) onTextChange { text -> listener?.onNameChange(1, text) @@ -73,6 +76,7 @@ class SpaceDefaultRoomEpoxyController @Inject constructor( enabled(true) value(data?.defaultRooms?.get(2)) hint(stringProvider.getString(R.string.create_room_name_section)) + endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT) showBottomSeparator(false) onTextChange { text -> listener?.onNameChange(2, text) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 41b489002b..5c6966ef4e 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3276,6 +3276,7 @@ What are some discussions you want to have in Runner’s World? We’ll create rooms for them, and auto-join everyone. You can add more later too. General + Random Creating Space… Invite people to your space It’s just you at the moment. %s will be even better with others. From a433f2f965355a0d06da06380f6a8efa34897433 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 26 Feb 2021 09:14:44 +0100 Subject: [PATCH 20/97] a11y --- vector/src/main/res/layout/item_editable_square_avatar.xml | 1 + vector/src/main/res/layout/item_space.xml | 2 +- vector/src/main/res/layout/item_space_subspace.xml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/res/layout/item_editable_square_avatar.xml b/vector/src/main/res/layout/item_editable_square_avatar.xml index b3ec057fd4..3112b36a65 100644 --- a/vector/src/main/res/layout/item_editable_square_avatar.xml +++ b/vector/src/main/res/layout/item_editable_square_avatar.xml @@ -57,6 +57,7 @@ android:backgroundTint="?riotx_background" android:padding="8dp" android:src="@drawable/ic_camera_plain" + android:contentDescription="@string/a11y_change_avatar" app:tint="?riotx_text_secondary" app:layout_constraintCircle="@+id/itemEditableAvatarImageContainer" app:layout_constraintCircleAngle="135" diff --git a/vector/src/main/res/layout/item_space.xml b/vector/src/main/res/layout/item_space.xml index f52ef31ead..25b685b999 100644 --- a/vector/src/main/res/layout/item_space.xml +++ b/vector/src/main/res/layout/item_space.xml @@ -50,7 +50,7 @@ android:layout_marginEnd="4dp" android:importantForAccessibility="no" android:src="@drawable/ic_room_actions_leave" - android:tint="@color/riotx_destructive_accent" + app:tint="@color/riotx_destructive_accent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/groupAvatarChevron" app:layout_constraintTop_toTopOf="parent" /> diff --git a/vector/src/main/res/layout/item_space_subspace.xml b/vector/src/main/res/layout/item_space_subspace.xml index ac654dc2b3..d3b243132e 100644 --- a/vector/src/main/res/layout/item_space_subspace.xml +++ b/vector/src/main/res/layout/item_space_subspace.xml @@ -28,6 +28,7 @@ android:layout_height="24dp" android:scaleType="centerInside" android:visibility="visible" + android:contentDescription="@string/a11y_image" android:layout_marginStart="8dp" app:layout_constraintBottom_toBottomOf="@id/childSpaceName" app:layout_constraintStart_toEndOf="@id/childSpaceTab" From 03ef480bea14a1c958655e0d71093eebcd88b1f2 Mon Sep 17 00:00:00 2001 From: Valere Date: Sat, 27 Feb 2021 11:17:19 +0100 Subject: [PATCH 21/97] Fix missing param in string --- .../features/spaces/create/SpaceDefaultRoomEpoxyController.kt | 2 +- vector/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt index 800de01b62..a1e46457e3 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt @@ -37,7 +37,7 @@ class SpaceDefaultRoomEpoxyController @Inject constructor( genericFooterItem { id("info_help_header") style(GenericItem.STYLE.BIG_TEXT) - text(stringProvider.getString(R.string.create_spaces_room_public_header)) + text(stringProvider.getString(R.string.create_spaces_room_public_header, data?.name)) textColor(colorProvider.getColorFromAttribute(R.attr.riot_primary_text_color)) } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 5c6966ef4e..272bec27fe 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3273,7 +3273,7 @@ Add some details to help it stand out. You can change these at any point. Add some details to help people identify it. You can change these at any point. Give it a name to continue. - What are some discussions you want to have in Runner’s World? + What are some discussions you want to have in %s? We’ll create rooms for them, and auto-join everyone. You can add more later too. General Random From ef42591534effb92d45c7e374d309d3e99a46a5d Mon Sep 17 00:00:00 2001 From: Valere Date: Sat, 27 Feb 2021 11:13:54 +0100 Subject: [PATCH 22/97] Open Space Link initial commit --- .../api/session/room/peeking/PeekResult.kt | 1 + .../session/room/peeking/PeekRoomTask.kt | 9 +- .../im/vector/app/core/di/FragmentModule.kt | 12 + .../vector/app/features/home/HomeActivity.kt | 17 ++ .../home/room/detail/RoomDetailFragment.kt | 4 +- .../app/features/matrixto/MatrixToAction.kt | 7 + .../features/matrixto/MatrixToBottomSheet.kt | 83 +++---- .../matrixto/MatrixToBottomSheetState.kt | 34 ++- .../matrixto/MatrixToBottomSheetViewModel.kt | 197 +++++++++++++++-- .../matrixto/MatrixToRoomSpaceFragment.kt | 207 ++++++++++++++++++ .../features/matrixto/MatrixToUserFragment.kt | 99 +++++++++ .../features/matrixto/MatrixToViewEvents.kt | 2 + .../features/permalink/PermalinkHandler.kt | 10 +- .../roomdirectory/PublicRoomsFragment.kt | 3 +- .../layout/bottom_sheet_matrix_to_card.xml | 88 +------- .../fragment_matrix_to_room_space_card.xml | 145 ++++++++++++ .../layout/fragment_matrix_to_user_card.xml | 86 ++++++++ vector/src/main/res/values/strings.xml | 3 + 18 files changed, 851 insertions(+), 156 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/matrixto/MatrixToUserFragment.kt create mode 100644 vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml create mode 100644 vector/src/main/res/layout/fragment_matrix_to_user_card.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt index a27e88aced..c213eee08d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt @@ -24,6 +24,7 @@ sealed class PeekResult { val topic: String?, val avatarUrl: String?, val numJoinedMembers: Int?, + val roomType: String?, val viaServers: List ) : PeekResult() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt index 5b211c505f..5d45259304 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility 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.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFilter import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.peeking.PeekResult @@ -100,7 +101,8 @@ internal class DefaultPeekRoomTask @Inject constructor( name = publicRepoResult.name, topic = publicRepoResult.topic, numJoinedMembers = publicRepoResult.numJoinedMembers, - viaServers = serverList + viaServers = serverList, + roomType = null // would be nice to get that from directory... ) } @@ -130,6 +132,10 @@ internal class DefaultPeekRoomTask @Inject constructor( .distinctBy { it.stateKey } .count() + val roomType = stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_CREATE } + ?.let { it.content?.toModel()?.type } + return PeekResult.Success( roomId = roomId, alias = alias, @@ -137,6 +143,7 @@ internal class DefaultPeekRoomTask @Inject constructor( name = name, topic = topic, numJoinedMembers = memberCount, + roomType = roomType, viaServers = serverList ) } catch (failure: Throwable) { 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 d3b46b5c64..a1be337dc6 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 @@ -72,6 +72,8 @@ import im.vector.app.features.login.LoginSplashFragment import im.vector.app.features.login.LoginWaitForEmailFragment import im.vector.app.features.login.LoginWebFragment import im.vector.app.features.login.terms.LoginTermsFragment +import im.vector.app.features.matrixto.MatrixToRoomSpaceFragment +import im.vector.app.features.matrixto.MatrixToUserFragment import im.vector.app.features.pin.PinFragment import im.vector.app.features.qrcode.QrCodeScannerFragment import im.vector.app.features.reactions.EmojiChooserFragment @@ -654,4 +656,14 @@ interface FragmentModule { @IntoMap @FragmentKey(CreateSpaceDefaultRoomsFragment::class) fun bindCreateSpaceDefaultRoomsFragment(fragment: CreateSpaceDefaultRoomsFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(MatrixToUserFragment::class) + fun bindMatrixToUserFragment(fragment: MatrixToUserFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(MatrixToRoomSpaceFragment::class) + fun bindMatrixToRoomSpaceFragment(fragment: MatrixToRoomSpaceFragment): Fragment } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index db7bb8fa16..84098b8281 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -451,6 +451,23 @@ class HomeActivity : return true } + override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { + if (roomId == null) return false + val listener = object : MatrixToBottomSheet.InteractionListener { + override fun navigateToRoom(roomId: String) { + navigator.openRoom(this@HomeActivity, roomId) + } + + override fun switchToSpace(spaceId: String) { + navigator.switchToSpace(this@HomeActivity, spaceId, null, false) + } + } + + MatrixToBottomSheet.withLink(deepLink.toString(), listener) + .show(supportFragmentManager, "HA#MatrixToBottomSheet") + return true + } + companion object { fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent { val args = HomeActivityArgs( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index a585d7609a..4d7f9b64f0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -1445,7 +1445,7 @@ class RoomDetailFragment @Inject constructor( override fun onUrlClicked(url: String, title: String): Boolean { permalinkHandler .launch(requireActivity(), url, object : NavigationInterceptor { - override fun navToRoom(roomId: String?, eventId: String?): Boolean { + override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { // Same room? if (roomId == roomDetailArgs.roomId) { // Navigation to same room @@ -1653,7 +1653,7 @@ class RoomDetailFragment @Inject constructor( override fun onRoomCreateLinkClicked(url: String) { permalinkHandler .launch(requireContext(), url, object : NavigationInterceptor { - override fun navToRoom(roomId: String?, eventId: String?): Boolean { + override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { requireActivity().finish() return false } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToAction.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToAction.kt index e1c6800494..1c9d0bbb18 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToAction.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToAction.kt @@ -21,4 +21,11 @@ import org.matrix.android.sdk.api.util.MatrixItem sealed class MatrixToAction : VectorViewModelAction { data class StartChattingWithUser(val matrixItem: MatrixItem) : MatrixToAction() + object FailedToResolveUser : MatrixToAction() + object FailedToStartChatting : MatrixToAction() + data class JoinSpace(val spaceID: String, val viaServers: List?) : MatrixToAction() + data class JoinRoom(val roomId: String, val viaServers: List?) : MatrixToAction() + data class OpenSpace(val spaceID: String) : MatrixToAction() + data class OpenRoom(val roomId: String) : MatrixToAction() +// data class OpenSpace(val spaceID: String) : MatrixToAction() } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt index 1d897477a2..b9ef833850 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt @@ -21,22 +21,23 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.view.isInvisible +import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading +import androidx.fragment.app.Fragment +import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.MvRx -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import im.vector.app.R import im.vector.app.core.di.ScreenComponent -import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetMatrixToCardBinding import im.vector.app.features.home.AvatarRenderer import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.session.permalinks.PermalinkData import javax.inject.Inject +import kotlin.reflect.KClass class MatrixToBottomSheet : VectorBaseBottomSheetDialogFragment() { @@ -65,63 +66,41 @@ class MatrixToBottomSheet : interface InteractionListener { fun navigateToRoom(roomId: String) + fun switchToSpace(spaceId: String) {} } override fun invalidate() = withState(viewModel) { state -> super.invalidate() - when (val item = state.matrixItem) { - Uninitialized -> { - views.matrixToCardContentLoading.isVisible = false - views.matrixToCardUserContentVisibility.isVisible = false + when (state.linkType) { + is PermalinkData.RoomLink -> { + views.matrixToCardContentLoading.isVisible = state.roomPeekResult is Incomplete + showFragment(MatrixToRoomSpaceFragment::class, Bundle()) } - is Loading -> { - views.matrixToCardContentLoading.isVisible = true - views.matrixToCardUserContentVisibility.isVisible = false + is PermalinkData.UserLink -> { + views.matrixToCardContentLoading.isVisible = state.matrixItem is Incomplete + showFragment(MatrixToUserFragment::class, Bundle()) } - is Success -> { - views.matrixToCardContentLoading.isVisible = false - views.matrixToCardUserContentVisibility.isVisible = true - views.matrixToCardNameText.setTextOrHide(item.invoke().displayName) - views.matrixToCardUserIdText.setTextOrHide(item.invoke().id) - avatarRenderer.render(item.invoke(), views.matrixToCardAvatar) + is PermalinkData.GroupLink -> { } - is Fail -> { - // TODO display some error copy? - dismiss() + is PermalinkData.FallbackLink -> { } } + } - when (state.startChattingState) { - Uninitialized -> { - views.matrixToCardButtonLoading.isVisible = false - views.matrixToCardSendMessageButton.isVisible = false - } - is Success -> { - views.matrixToCardButtonLoading.isVisible = false - views.matrixToCardSendMessageButton.isVisible = true - } - is Fail -> { - views.matrixToCardButtonLoading.isVisible = false - views.matrixToCardSendMessageButton.isVisible = true - // TODO display some error copy? - dismiss() - } - is Loading -> { - views.matrixToCardButtonLoading.isVisible = true - views.matrixToCardSendMessageButton.isInvisible = true + private fun showFragment(fragmentClass: KClass, bundle: Bundle) { + if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { + childFragmentManager.commitTransaction { + replace(views.matrixToCardFragmentContainer.id, + fragmentClass.java, + bundle, + fragmentClass.simpleName + ) } } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - views.matrixToCardSendMessageButton.debouncedClicks { - withState(viewModel) { - it.matrixItem.invoke()?.let { item -> - viewModel.handle(MatrixToAction.StartChattingWithUser(item)) - } - } - } viewModel.observeViewEvents { when (it) { @@ -130,6 +109,16 @@ class MatrixToBottomSheet : dismiss() } MatrixToViewEvents.Dismiss -> dismiss() + is MatrixToViewEvents.NavigateToSpace -> { + interactionListener?.switchToSpace(it.spaceId) + dismiss() + } + is MatrixToViewEvents.ShowModalError -> { + AlertDialog.Builder(requireContext()) + .setMessage(it.error) + .setPositiveButton(getString(R.string.ok), null) + .show() + } } } } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt index 9b1ce9fea8..c0d42d60dd 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt @@ -19,15 +19,45 @@ package im.vector.app.features.matrixto import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.api.session.permalinks.PermalinkData +import org.matrix.android.sdk.api.session.permalinks.PermalinkParser +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.util.MatrixItem data class MatrixToBottomSheetState( val deepLink: String, + val linkType: PermalinkData, val matrixItem: Async = Uninitialized, - val startChattingState: Async = Uninitialized + val startChattingState: Async = Uninitialized, + val roomPeekResult: Async = Uninitialized ) : MvRxState { constructor(args: MatrixToBottomSheet.MatrixToArgs) : this( - deepLink = args.matrixToLink + deepLink = args.matrixToLink, + linkType = PermalinkParser.parse(args.matrixToLink) ) } + +sealed class RoomInfoResult { + data class FullInfo( + val roomItem: MatrixItem.RoomItem, + val name: String, + val topic: String, + val memberCount: Int?, + val alias: String?, + val membership: Membership, + val roomType: String?, + val viaServers: List? + ) : RoomInfoResult() + + data class PartialInfo( + val roomId: String?, + val viaServers: List + ) : RoomInfoResult() + + data class UnknownAlias( + val alias: String? + ) : RoomInfoResult() + + object NotFound : RoomInfoResult() +} diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt index b961383575..267322b58a 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt @@ -25,9 +25,10 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider @@ -35,19 +36,26 @@ import im.vector.app.features.createdirect.DirectRoomHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.permalinks.PermalinkData -import org.matrix.android.sdk.api.session.permalinks.PermalinkParser +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.peeking.PeekResult +import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription +import org.matrix.android.sdk.internal.util.awaitCallback +import timber.log.Timber class MatrixToBottomSheetViewModel @AssistedInject constructor( @Assisted initialState: MatrixToBottomSheetState, private val session: Session, private val stringProvider: StringProvider, private val directRoomHelper: DirectRoomHelper, - private val rawService: RawService) : VectorViewModel(initialState) { + private val errorFormatter: ErrorFormatter) + : VectorViewModel(initialState) { @AssistedFactory interface Factory { @@ -55,8 +63,23 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( } init { - setState { - copy(matrixItem = Loading()) + when (initialState.linkType) { + is PermalinkData.RoomLink -> { + setState { + copy(roomPeekResult = Loading()) + } + } + is PermalinkData.UserLink -> { + setState { + copy(matrixItem = Loading()) + } + } + is PermalinkData.GroupLink -> { + // Not yet supported + } + is PermalinkData.FallbackLink -> { + // Not yet supported + } } viewModelScope.launch(Dispatchers.IO) { resolveLink(initialState) @@ -64,7 +87,7 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( } private suspend fun resolveLink(initialState: MatrixToBottomSheetState) { - val permalinkData = PermalinkParser.parse(initialState.deepLink) + val permalinkData = initialState.linkType if (permalinkData is PermalinkData.FallbackLink) { setState { copy( @@ -75,8 +98,8 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( return } - when (permalinkData) { - is PermalinkData.UserLink -> { + when (permalinkData) { + is PermalinkData.UserLink -> { val user = resolveUser(permalinkData.userId) setState { copy( @@ -85,11 +108,79 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( ) } } - is PermalinkData.RoomLink -> { - // not yet supported - _viewEvents.post(MatrixToViewEvents.Dismiss) + is PermalinkData.RoomLink -> { + // could this room be already known + val knownRoom = if (permalinkData.isRoomAlias) { + tryOrNull { + awaitCallback> { + session.getRoomIdByAlias(permalinkData.roomIdOrAlias, false, it) + } + } + ?.getOrNull() + ?.roomId?.let { + session.getRoom(permalinkData.roomIdOrAlias) + } + } else { + session.getRoom(permalinkData.roomIdOrAlias) + }?.roomSummary() + + if (knownRoom != null) { + setState { + copy( + roomPeekResult = Success( + RoomInfoResult.FullInfo( + roomItem = knownRoom.toMatrixItem(), + name = knownRoom.name, + topic = knownRoom.topic, + memberCount = knownRoom.joinedMembersCount, + alias = knownRoom.canonicalAlias, + membership = knownRoom.membership, + roomType = knownRoom.roomType, + viaServers = null + ) + ) + ) + } + } else { + + val result = when (val peekResult = tryOrNull { resolveRoom(permalinkData.roomIdOrAlias) }) { + is PeekResult.Success -> { + RoomInfoResult.FullInfo( + roomItem = MatrixItem.RoomItem(peekResult.roomId, peekResult.name, peekResult.avatarUrl), + name = peekResult.name ?: "", + topic = peekResult.topic ?: "", + memberCount = peekResult.numJoinedMembers, + alias = peekResult.alias, + membership = Membership.NONE, + roomType = peekResult.roomType, + viaServers = peekResult.viaServers.takeIf { it.isNotEmpty() } ?: permalinkData.viaParameters + ) + } + is PeekResult.PeekingNotAllowed -> { + RoomInfoResult.PartialInfo( + roomId = permalinkData.roomIdOrAlias, + viaServers = permalinkData.viaParameters + ) + } + PeekResult.UnknownAlias -> { + RoomInfoResult.NotFound + } + null -> { + RoomInfoResult.PartialInfo( + roomId = permalinkData.roomIdOrAlias, + viaServers = permalinkData.viaParameters + ).takeIf { permalinkData.isRoomAlias.not() } + ?: RoomInfoResult.NotFound + } + } + setState { + copy( + roomPeekResult = Success(result) + ) + } + } } - is PermalinkData.GroupLink -> { + is PermalinkData.GroupLink -> { // not yet supported _viewEvents.post(MatrixToViewEvents.Dismiss) } @@ -105,6 +196,19 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( ?: User(userId, null, null) } + /** + * Let's try to get some information about that room, + * main thing is trying to see if it's a space or a room + */ + private suspend fun resolveRoom(roomIdOrAlias: String): PeekResult { + return tryOrNull { // this should not throw as it returns a result, but better be safe + awaitCallback { + session.peekRoom(roomIdOrAlias, it) + } + } + ?: PeekResult.PeekingNotAllowed(roomIdOrAlias, null, emptyList()) + } + companion object : MvRxViewModelFactory { override fun create(viewModelContext: ViewModelContext, state: MatrixToBottomSheetState): MatrixToBottomSheetViewModel? { val fragment: MatrixToBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() @@ -116,14 +220,75 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( override fun handle(action: MatrixToAction) { when (action) { is MatrixToAction.StartChattingWithUser -> handleStartChatting(action) + MatrixToAction.FailedToResolveUser -> { + _viewEvents.post(MatrixToViewEvents.Dismiss) + } + MatrixToAction.FailedToStartChatting -> { + _viewEvents.post(MatrixToViewEvents.Dismiss) + } + is MatrixToAction.JoinSpace -> handleJoinSpace(action) + is MatrixToAction.JoinRoom -> handleJoinRoom(action) + is MatrixToAction.OpenSpace -> { + _viewEvents.post(MatrixToViewEvents.NavigateToSpace(action.spaceID)) + } + is MatrixToAction.OpenRoom -> { + _viewEvents.post(MatrixToViewEvents.NavigateToRoom(action.roomId)) + } }.exhaustive } - private fun handleStartChatting(action: MatrixToAction.StartChattingWithUser) { + private fun handleJoinSpace(joinSpace: MatrixToAction.JoinSpace) { + setState { + copy(startChattingState = Loading()) + } viewModelScope.launch { - setState { - copy(startChattingState = Loading()) + try { + val joinResult = session.spaceService().joinSpace(joinSpace.spaceID, null, joinSpace.viaServers?.take(3) ?: emptyList()) + if (joinResult.isSuccess()) { + _viewEvents.post(MatrixToViewEvents.NavigateToSpace(joinSpace.spaceID)) + } else { + val errMsg = errorFormatter.toHumanReadable((joinResult as? SpaceService.JoinSpaceResult.Fail)?.error) + _viewEvents.post(MatrixToViewEvents.ShowModalError(errMsg)) + } + } catch (failure: Throwable) { + _viewEvents.post(MatrixToViewEvents.ShowModalError(errorFormatter.toHumanReadable(failure))) + } finally { + setState { + // we can hide this button has we will navigate out + copy(startChattingState = Uninitialized) + } } + + } + } + + private fun handleJoinRoom(action: MatrixToAction.JoinRoom) { + setState { + copy(startChattingState = Loading()) + } + viewModelScope.launch { + try { + awaitCallback { + session.joinRoom(action.roomId, null, action.viaServers?.take(3) ?: emptyList(), it) + } + _viewEvents.post(MatrixToViewEvents.NavigateToRoom(action.roomId)) + } catch (failure: Throwable) { + _viewEvents.post(MatrixToViewEvents.ShowModalError(errorFormatter.toHumanReadable(failure))) + } finally { + setState { + // we can hide this button has we will navigate out + copy(startChattingState = Uninitialized) + } + } + + } + } + + private fun handleStartChatting(action: MatrixToAction.StartChattingWithUser) { + setState { + copy(startChattingState = Loading()) + } + viewModelScope.launch { val roomId = try { directRoomHelper.ensureDMExists(action.matrixItem.id) } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt new file mode 100644 index 0000000000..8ab2a46bad --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.matrixto + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentMatrixToRoomSpaceCardBinding +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomType +import javax.inject.Inject + +class MatrixToRoomSpaceFragment @Inject constructor( + private val avatarRenderer: AvatarRenderer +) : VectorBaseFragment() { + + private val sharedViewModel: MatrixToBottomSheetViewModel by parentFragmentViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentMatrixToRoomSpaceCardBinding { + return FragmentMatrixToRoomSpaceCardBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.matrixToCardMainButton.debouncedClicks { + mainButtonClicked() + } + views.matrixToCardSecondaryButton.debouncedClicks { + secondaryButtonClicked() + } + } + + override fun invalidate() = withState(sharedViewModel) { state -> + when (val item = state.roomPeekResult) { + Uninitialized -> { + views.matrixToCardContentVisibility.isVisible = false + } + is Loading -> { + views.matrixToCardContentVisibility.isVisible = false + } + is Success -> { + views.matrixToCardContentVisibility.isVisible = true + when (val peek = item.invoke()) { + is RoomInfoResult.FullInfo -> { + val matrixItem = peek.roomItem + if (peek.roomType == RoomType.SPACE) { + avatarRenderer.renderSpace(matrixItem, views.matrixToCardAvatar) + } else { + avatarRenderer.render(matrixItem, views.matrixToCardAvatar) + } + views.matrixToCardNameText.setTextOrHide(peek.name) + views.matrixToCardAliasText.setTextOrHide(peek.alias) + views.matrixToCardDescText.setTextOrHide(peek.topic) + val memberCount = peek.memberCount + if (memberCount != null) { + views.matrixToMemberPills.isVisible = true + views.spaceChildMemberCountText.text = resources.getQuantityString(R.plurals.room_title_members, memberCount, memberCount) + } else { + // hide the pill + views.matrixToMemberPills.isVisible = false + } + + when (peek.membership) { + Membership.LEAVE, + Membership.NONE -> { + views.matrixToCardMainButton.isVisible = true + views.matrixToCardMainButton.text = getString(R.string.join_space) + views.matrixToCardSecondaryButton.isVisible = false + } + Membership.INVITE -> { + views.matrixToCardMainButton.isVisible = true + views.matrixToCardSecondaryButton.isVisible = true + views.matrixToCardMainButton.text = getString(R.string.join_space) + views.matrixToCardSecondaryButton.text = getString(R.string.decline) + } + Membership.JOIN -> { + views.matrixToCardMainButton.isVisible = true + views.matrixToCardSecondaryButton.isVisible = false + views.matrixToCardMainButton.text = getString(R.string.action_open) + } + Membership.KNOCK, + Membership.BAN -> { + // What to do here ? + views.matrixToCardMainButton.isVisible = false + views.matrixToCardSecondaryButton.isVisible = false + } + } + } + is RoomInfoResult.PartialInfo -> { + // It may still be possible to join + views.matrixToCardNameText.text = peek.roomId + views.matrixToCardAliasText.isVisible = false + views.matrixToMemberPills.isVisible = false + views.matrixToCardDescText.setTextOrHide(getString(R.string.room_preview_no_preview)) + + views.matrixToCardMainButton.text = getString(R.string.join_anyway) + views.matrixToCardSecondaryButton.isVisible = false + } + RoomInfoResult.NotFound -> { + // we cannot join :/ + views.matrixToCardNameText.isVisible = false + views.matrixToCardAliasText.isVisible = false + views.matrixToMemberPills.isVisible = false + views.matrixToCardDescText.setTextOrHide(getString(R.string.room_preview_not_found)) + + views.matrixToCardMainButton.isVisible = false + views.matrixToCardSecondaryButton.isVisible = false + } + is RoomInfoResult.UnknownAlias -> { + views.matrixToCardNameText.isVisible = false + views.matrixToCardAliasText.isVisible = false + views.spaceChildMemberCountText.isVisible = false + views.matrixToCardDescText.setTextOrHide(getString(R.string.room_alias_preview_not_found)) + + views.matrixToCardMainButton.isVisible = false + views.matrixToCardSecondaryButton.isVisible = false + } + } + } + is Fail -> { + // TODO display some error copy? + sharedViewModel.handle(MatrixToAction.FailedToResolveUser) + } + } + + when (state.startChattingState) { + Uninitialized -> { + views.matrixToCardButtonLoading.isVisible = false +// views.matrixToCardMainButton.isVisible = false + } + is Success -> { + views.matrixToCardButtonLoading.isVisible = false + views.matrixToCardMainButton.isVisible = true + } + is Fail -> { + views.matrixToCardButtonLoading.isVisible = false + views.matrixToCardMainButton.isVisible = true + // TODO display some error copy? + } + is Loading -> { + views.matrixToCardButtonLoading.isVisible = true + views.matrixToCardMainButton.isInvisible = true + } + } + } + + private fun mainButtonClicked() = withState(sharedViewModel) { state -> + when (val info = state.roomPeekResult.invoke()) { + is RoomInfoResult.FullInfo -> { + when (info.membership) { + Membership.NONE, + Membership.INVITE, + Membership.LEAVE -> { + if (info.roomType == RoomType.SPACE) { + sharedViewModel.handle(MatrixToAction.JoinSpace(info.roomItem.id, info.viaServers)) + } else { + sharedViewModel.handle(MatrixToAction.JoinRoom(info.roomItem.id, info.viaServers)) + } + } + Membership.JOIN -> { + if (info.roomType == RoomType.SPACE) { + sharedViewModel.handle(MatrixToAction.OpenSpace(info.roomItem.id)) + } else { + sharedViewModel.handle(MatrixToAction.OpenRoom(info.roomItem.id)) + } + } + else -> { + } + } + } + is RoomInfoResult.PartialInfo -> { + } + else -> { + } + } + } + + fun secondaryButtonClicked() = withState(sharedViewModel) { state -> + } +} diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToUserFragment.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToUserFragment.kt new file mode 100644 index 0000000000..3792183bca --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToUserFragment.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.matrixto + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentMatrixToUserCardBinding +import im.vector.app.features.home.AvatarRenderer +import javax.inject.Inject + +class MatrixToUserFragment @Inject constructor( + private val avatarRenderer: AvatarRenderer +) : VectorBaseFragment() { + + private val sharedViewModel: MatrixToBottomSheetViewModel by parentFragmentViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentMatrixToUserCardBinding { + return FragmentMatrixToUserCardBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.matrixToCardSendMessageButton.debouncedClicks { + withState(sharedViewModel) { + it.matrixItem.invoke()?.let { item -> + sharedViewModel.handle(MatrixToAction.StartChattingWithUser(item)) + } + } + } + } + + override fun invalidate() = withState(sharedViewModel) { state -> + when (val item = state.matrixItem) { + Uninitialized -> { + views.matrixToCardUserContentVisibility.isVisible = false + } + is Loading -> { + views.matrixToCardUserContentVisibility.isVisible = false + } + is Success -> { + views.matrixToCardUserContentVisibility.isVisible = true + views.matrixToCardNameText.setTextOrHide(item.invoke().displayName) + views.matrixToCardUserIdText.setTextOrHide(item.invoke().id) + avatarRenderer.render(item.invoke(), views.matrixToCardAvatar) + } + is Fail -> { + // TODO display some error copy? + sharedViewModel.handle(MatrixToAction.FailedToResolveUser) + } + } + + when (state.startChattingState) { + Uninitialized -> { + views.matrixToCardButtonLoading.isVisible = false + views.matrixToCardSendMessageButton.isVisible = false + } + is Success -> { + views.matrixToCardButtonLoading.isVisible = false + views.matrixToCardSendMessageButton.isVisible = true + } + is Fail -> { + views.matrixToCardButtonLoading.isVisible = false + views.matrixToCardSendMessageButton.isVisible = true + // TODO display some error copy? + sharedViewModel.handle(MatrixToAction.FailedToStartChatting) + } + is Loading -> { + views.matrixToCardButtonLoading.isVisible = true + views.matrixToCardSendMessageButton.isInvisible = true + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToViewEvents.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToViewEvents.kt index f9491fd361..2c7bc07b23 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToViewEvents.kt @@ -20,5 +20,7 @@ import im.vector.app.core.platform.VectorViewEvents sealed class MatrixToViewEvents : VectorViewEvents { data class NavigateToRoom(val roomId: String) : MatrixToViewEvents() + data class NavigateToSpace(val spaceId: String) : MatrixToViewEvents() + data class ShowModalError(val error: String) : MatrixToViewEvents() object Dismiss : MatrixToViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt index d43337fab0..ca1a010a03 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt @@ -78,12 +78,12 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti buildTask: Boolean ): Single { return when (permalinkData) { - is PermalinkData.RoomLink -> { + is PermalinkData.RoomLink -> { permalinkData.getRoomId() .observeOn(AndroidSchedulers.mainThread()) .map { val roomId = it.getOrNull() - if (navigationInterceptor?.navToRoom(roomId, permalinkData.eventId) != true) { + if (navigationInterceptor?.navToRoom(roomId, permalinkData.eventId, rawLink) != true) { openRoom( context = context, roomId = roomId, @@ -94,11 +94,11 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti true } } - is PermalinkData.GroupLink -> { + is PermalinkData.GroupLink -> { navigator.openGroupDetail(permalinkData.groupId, context, buildTask) Single.just(true) } - is PermalinkData.UserLink -> { + is PermalinkData.UserLink -> { if (navigationInterceptor?.navToMemberProfile(permalinkData.userId, rawLink) != true) { navigator.openRoomMemberProfile(userId = permalinkData.userId, roomId = null, context = context, buildTask = buildTask) } @@ -196,7 +196,7 @@ interface NavigationInterceptor { /** * Return true if the navigation has been intercepted */ - fun navToRoom(roomId: String?, eventId: String? = null): Boolean { + fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri? = null): Boolean { return false } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt index cee30e7def..8214b26fea 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.roomdirectory +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.MenuItem @@ -127,7 +128,7 @@ class PublicRoomsFragment @Inject constructor( val permalink = session.permalinkService().createPermalink(roomIdOrAlias) permalinkHandler .launch(requireContext(), permalink, object : NavigationInterceptor { - override fun navToRoom(roomId: String?, eventId: String?): Boolean { + override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { requireActivity().finish() return false } diff --git a/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml b/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml index 37f9633728..652ec8421d 100644 --- a/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml +++ b/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml @@ -1,6 +1,5 @@ - - - - + android:layout_height="wrap_content" /> - - - - - - - - - + diff --git a/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml b/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml new file mode 100644 index 0000000000..6f49f15842 --- /dev/null +++ b/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_matrix_to_user_card.xml b/vector/src/main/res/layout/fragment_matrix_to_user_card.xml new file mode 100644 index 0000000000..03599779f1 --- /dev/null +++ b/vector/src/main/res/layout/fragment_matrix_to_user_card.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 272bec27fe..93c42dc723 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3286,6 +3286,9 @@ Join my space %1$s %2$s Skip for now + Join Space + Join Anyway + This alias is not accessible at this time.\nTry again later, or ask a room admin to check if you have access. From bb0b1ed0983059be2e2584b74ef430e797f5ef81 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 3 Mar 2021 15:16:46 +0100 Subject: [PATCH 23/97] Some fixes + join actions --- .../matrixto/MatrixToBottomSheetViewModel.kt | 15 ++----- .../matrixto/MatrixToRoomSpaceFragment.kt | 14 ++++++- .../features/navigation/DefaultNavigator.kt | 24 +++++++++++ .../app/features/navigation/Navigator.kt | 2 + .../features/permalink/PermalinkHandler.kt | 40 +++++-------------- 5 files changed, 51 insertions(+), 44 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt index 267322b58a..e608774f33 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt @@ -47,7 +47,6 @@ import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription import org.matrix.android.sdk.internal.util.awaitCallback -import timber.log.Timber class MatrixToBottomSheetViewModel @AssistedInject constructor( @Assisted initialState: MatrixToBottomSheetState, @@ -118,7 +117,7 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( } ?.getOrNull() ?.roomId?.let { - session.getRoom(permalinkData.roomIdOrAlias) + session.getRoom(it) } } else { session.getRoom(permalinkData.roomIdOrAlias) @@ -142,7 +141,6 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( ) } } else { - val result = when (val peekResult = tryOrNull { resolveRoom(permalinkData.roomIdOrAlias) }) { is PeekResult.Success -> { RoomInfoResult.FullInfo( @@ -163,7 +161,7 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( ) } PeekResult.UnknownAlias -> { - RoomInfoResult.NotFound + RoomInfoResult.UnknownAlias(permalinkData.roomIdOrAlias) } null -> { RoomInfoResult.PartialInfo( @@ -201,12 +199,9 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( * main thing is trying to see if it's a space or a room */ private suspend fun resolveRoom(roomIdOrAlias: String): PeekResult { - return tryOrNull { // this should not throw as it returns a result, but better be safe - awaitCallback { - session.peekRoom(roomIdOrAlias, it) - } + return awaitCallback { + session.peekRoom(roomIdOrAlias, it) } - ?: PeekResult.PeekingNotAllowed(roomIdOrAlias, null, emptyList()) } companion object : MvRxViewModelFactory { @@ -258,7 +253,6 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( copy(startChattingState = Uninitialized) } } - } } @@ -280,7 +274,6 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( copy(startChattingState = Uninitialized) } } - } } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt index 8ab2a46bad..31dc537395 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt @@ -87,17 +87,23 @@ class MatrixToRoomSpaceFragment @Inject constructor( views.matrixToMemberPills.isVisible = false } + val joinTextRes = if (peek.roomType == RoomType.SPACE) { + R.string.join_space + } else { + R.string.join_room + } + when (peek.membership) { Membership.LEAVE, Membership.NONE -> { views.matrixToCardMainButton.isVisible = true - views.matrixToCardMainButton.text = getString(R.string.join_space) + views.matrixToCardMainButton.text = getString(joinTextRes) views.matrixToCardSecondaryButton.isVisible = false } Membership.INVITE -> { views.matrixToCardMainButton.isVisible = true views.matrixToCardSecondaryButton.isVisible = true - views.matrixToCardMainButton.text = getString(R.string.join_space) + views.matrixToCardMainButton.text = getString(joinTextRes) views.matrixToCardSecondaryButton.text = getString(R.string.decline) } Membership.JOIN -> { @@ -196,6 +202,10 @@ class MatrixToRoomSpaceFragment @Inject constructor( } } is RoomInfoResult.PartialInfo -> { + // we can try to join anyway + if (info.roomId != null) { + sharedViewModel.handle(MatrixToAction.JoinRoom(info.roomId, info.viaServers)) + } } else -> { } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 80cb82098b..27c29ae42b 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -54,6 +54,7 @@ import im.vector.app.features.home.room.detail.search.SearchActivity import im.vector.app.features.home.room.detail.search.SearchArgs import im.vector.app.features.home.room.filtered.FilteredRoomsActivity import im.vector.app.features.invite.InviteUsersToRoomActivity +import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.BigImageViewerActivity import im.vector.app.features.media.VectorAttachmentViewerActivity @@ -119,6 +120,12 @@ class DefaultNavigator @Inject constructor( val args = RoomDetailArgs(roomId, eventId = null, openShareSpaceForId = spaceId.takeIf { openShareSheet }) val intent = RoomDetailActivity.newIntent(context, args) startActivity(context, intent, false) + } else { + // go back to home if we are showing room details? + // This is a bit ugly, but the navigator is supposed to know about the activity stack + if (context is RoomDetailActivity) { + context.finish() + } } } @@ -225,6 +232,23 @@ class DefaultNavigator @Inject constructor( context.startActivity(intent) } + override fun openMatrixToBottomSheet(context: Context, link: String) { + if (context is AppCompatActivity) { + val listener = object : MatrixToBottomSheet.InteractionListener { + override fun navigateToRoom(roomId: String) { + openRoom(context, roomId) + } + + override fun switchToSpace(spaceId: String) { + this@DefaultNavigator.switchToSpace(context, spaceId, null, openShareSheet = false) + } + } + // TODO check if there is already one?? + MatrixToBottomSheet.withLink(link, listener) + .show(context.supportFragmentManager, "HA#MatrixToBottomSheet") + } + } + override fun openRoomDirectory(context: Context, initialFilter: String) { val intent = RoomDirectoryActivity.getIntent(context, initialFilter) context.startActivity(intent) diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 225a2b89ab..489cd37987 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -58,6 +58,8 @@ interface Navigator { fun openRoomPreview(context: Context, roomPreviewData: RoomPreviewData) + fun openMatrixToBottomSheet(context: Context, link: String) + fun openCreateRoom(context: Context, initialName: String = "") fun openCreateDirectRoom(context: Context) diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt index ca1a010a03..1fea25159f 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt @@ -22,7 +22,6 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.utils.toast import im.vector.app.features.navigation.Navigator -import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers @@ -88,6 +87,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti context = context, roomId = roomId, permalinkData = permalinkData, + rawLink = rawLink, buildTask = buildTask ) } @@ -137,6 +137,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti context: Context, roomId: String?, permalinkData: PermalinkData.RoomLink, + rawLink: Uri, buildTask: Boolean ) { val session = activeSessionHolder.getSafeActiveSession() ?: return @@ -152,40 +153,17 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti return when { membership == Membership.BAN -> context.toast(R.string.error_opening_banned_room) membership?.isActive().orFalse() -> { - if (isSpace) { -// navigator.switchToSpace(context, roomId, null) - navigator.openSpacePreview(context, roomId) - } else { + if (!isSpace && membership == Membership.JOIN) { + // If it's a room you're in, let's just open it, you can tap back if needed navigator.openRoom(context, roomId, eventId, buildTask) + } else { + // maybe open space preview navigator.openSpacePreview(context, roomId)? if already joined? + navigator.openMatrixToBottomSheet(context, rawLink.toString()) } } else -> { - if (isSpace) { - navigator.openSpacePreview(context, roomId) - } else { - if (roomSummary == null) { - // we don't know this room, try to peek - val roomPreviewData = RoomPreviewData( - roomId = roomId, - roomAlias = roomAlias, - peekFromServer = true, - buildTask = buildTask, - homeServers = permalinkData.viaParameters - ) - navigator.openRoomPreview(context, roomPreviewData) - } else { - val roomPreviewData = RoomPreviewData( - roomId = roomId, - eventId = eventId, - roomAlias = roomAlias ?: roomSummary.canonicalAlias, - roomName = roomSummary.displayName, - avatarUrl = roomSummary.avatarUrl, - buildTask = buildTask, - homeServers = permalinkData.viaParameters - ) - navigator.openRoomPreview(context, roomPreviewData) - } - } + // XXX this could trigger another server load + navigator.openMatrixToBottomSheet(context, rawLink.toString()) } } } From 80f1c6cb2d08b4edd4af2798d6f453ec6a004e70 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 11 Mar 2021 22:15:40 +0100 Subject: [PATCH 24/97] post rebase fix --- .../vector/app/features/home/room/detail/RoomDetailFragment.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 4d7f9b64f0..df38511b70 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -95,9 +95,7 @@ import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.CurrentCallsView import im.vector.app.core.ui.views.JumpToReadMarkerView import im.vector.app.core.ui.views.KnownCallsViewHolder -import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.FailedMessagesWarningView -import im.vector.app.core.ui.views.JumpToReadMarkerView import im.vector.app.core.ui.views.NotificationAreaView import im.vector.app.core.utils.Debouncer import im.vector.app.core.utils.DimensionConverter From 0c5ca9f51b97431ddad655a927fcbe7909d3f26e Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 11 Mar 2021 22:35:11 +0100 Subject: [PATCH 25/97] Space hierachy SDK updates --- .../org/matrix/android/sdk/rx/RxSession.kt | 10 +- .../sdk/session/space/SpaceHierarchyTest.kt | 373 ++++++++++++++++++ .../android/sdk/api/session/room/Room.kt | 3 + .../sdk/api/session/room/RoomService.kt | 5 + .../session/room/RoomSummaryQueryParams.kt | 25 +- .../sdk/api/session/room/model/RoomSummary.kt | 26 +- .../api/session/room/model/SpaceChildInfo.kt | 10 +- .../{IRoomSummary.kt => SpaceParentInfo.kt} | 21 +- .../android/sdk/api/session/space/Space.kt | 4 +- .../sdk/api/session/space/SpaceService.kt | 14 +- .../sdk/api/session/space/SpaceSummary.kt | 27 -- .../session/space/model/SpaceParentContent.kt | 6 - .../matrix/android/sdk/api/util/MatrixItem.kt | 3 - .../database/RealmSessionStoreMigration.kt | 38 +- .../database/mapper/RoomSummaryMapper.kt | 26 +- .../database/mapper/SpaceSummaryMapper.kt | 40 -- .../sdk/internal/database/model/RoomEntity.kt | 1 - .../database/model/RoomSummaryEntity.kt | 4 +- .../database/model/SessionRealmModule.kt | 4 +- ...foEntity.kt => SpaceChildSummaryEntity.kt} | 20 +- .../model/SpaceParentSummaryEntity.kt | 49 +++ .../database/model/SpaceSummaryEntity.kt | 41 -- .../query/SpaceSummaryEntityQueries.kt | 55 --- .../sdk/internal/session/room/DefaultRoom.kt | 11 + .../session/room/DefaultRoomService.kt | 20 +- .../sdk/internal/session/room/SpaceGetter.kt | 4 +- ...shipHelper.kt => RoomChildRelationInfo.kt} | 39 +- .../room/summary/HierarchyLiveDataHelper.kt | 67 ++++ .../room/summary/RoomSummaryDataSource.kt | 166 +++++++- .../room/summary/RoomSummaryUpdater.kt | 85 ++-- .../internal/session/space/CreateSpaceTask.kt | 8 +- .../internal/session/space/DefaultSpace.kt | 7 +- .../session/space/DefaultSpaceService.kt | 85 +++- .../internal/session/space/JoinSpaceTask.kt | 37 +- .../session/space/SpaceSummaryDataSource.kt | 93 ----- .../internal/session/sync/RoomSyncHandler.kt | 3 + .../grouplist/SelectedSpaceDataSource.kt | 4 +- .../app/features/home/HomeDetailFragment.kt | 6 +- .../app/features/home/HomeDetailViewState.kt | 3 +- .../timeline/factory/TimelineItemFactory.kt | 10 +- .../timeline/format/NoticeEventFormatter.kt | 2 + .../matrixto/MatrixToRoomSpaceFragment.kt | 2 +- .../features/navigation/DefaultNavigator.kt | 2 +- .../settings/SharedPreferenceLiveData.kt | 54 +++ .../features/settings/VectorPreferences.kt | 9 + .../settings/VectorSettingsLabsFragment.kt | 10 +- .../features/spaces/ShareSpaceBottomSheet.kt | 2 +- .../app/features/spaces/SpaceListFragment.kt | 6 +- .../features/spaces/SpaceSummaryController.kt | 22 +- .../app/features/spaces/SpaceSummaryItem.kt | 25 ++ .../features/spaces/SpacesListViewModel.kt | 55 ++- .../spaces/explore/SpaceDirectoryViewModel.kt | 4 +- .../spaces/preview/SpacePreviewViewModel.kt | 12 +- vector/src/main/res/layout/item_group.xml | 2 +- vector/src/main/res/layout/item_space.xml | 20 +- vector/src/main/res/values/strings.xml | 3 +- .../src/main/res/xml/vector_settings_labs.xml | 3 +- 57 files changed, 1211 insertions(+), 475 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/{IRoomSummary.kt => SpaceParentInfo.kt} (61%) delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceSummary.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/SpaceSummaryMapper.kt rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/{SpaceChildInfoEntity.kt => SpaceChildSummaryEntity.kt} (69%) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceSummaryEntity.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/SpaceSummaryEntityQueries.kt rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/{RoomRelationshipHelper.kt => RoomChildRelationInfo.kt} (64%) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/HierarchyLiveDataHelper.kt delete 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/settings/SharedPreferenceLiveData.kt 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 7cc0d69bb9..38b0a3a343 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,7 +39,6 @@ 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 @@ -68,13 +67,20 @@ class RxSession(private val session: Session) { } } - fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Observable> { + fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Observable> { return session.spaceService().getSpaceSummariesLive(queryParams).asObservable() .startWithCallable { session.spaceService().getSpaceSummaries(queryParams) } } + fun liveFlattenRoomSummaryChildOf(spaceId: String?): Observable> { + return session.getFlattenRoomSummaryChildOfLive(spaceId).asObservable() + .startWithCallable { + session.getFlattenRoomSummaryChildOf(spaceId) + } + } + fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable> { return session.getBreadcrumbsLive(queryParams).asObservable() .startWithCallable { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt new file mode 100644 index 0000000000..b512983ea6 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt @@ -0,0 +1,373 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.space + +import android.util.Log +import androidx.lifecycle.Observer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class SpaceHierarchyTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + + @Test + fun createCanonicalChildRelation() { + val session = commonTestHelper.createAccount("Jhon", SessionTestParams(true)) + val spaceName = "My Space" + val topic = "A public space for test" + val spaceId: String + runBlocking { + spaceId = session.spaceService().createSpace(spaceName, topic, null, true) + // wait a bit to let the summry update it self :/ + delay(400) + } + + val syncedSpace = session.spaceService().getSpace(spaceId) + + val roomId = commonTestHelper.doSync { + session.createRoom(CreateRoomParams().apply { name = "General" }, it) + } + + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + runBlocking { + syncedSpace!!.addChildren(roomId, viaServers, null, true) + } + + runBlocking { + session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers) + } + + Thread.sleep(9000) + + val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents + val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true } + + parents?.forEach { + Log.d("## TEST", "parent : $it") + } + + assertNotNull(parents) + assertEquals(1, parents.size) + assertEquals(spaceName, parents.first().roomSummary?.name) + + assertNotNull(canonicalParents) + assertEquals(1, canonicalParents.size) + assertEquals(spaceName, canonicalParents.first().roomSummary?.name) + } + + @Test + fun testCreateChildRelations() { + val session = commonTestHelper.createAccount("Jhon", SessionTestParams(true)) + val spaceName = "My Space" + val topic = "A public space for test" + Log.d("## TEST", "Before") + val spaceId = runBlocking { + session.spaceService().createSpace(spaceName, topic, null, true) + } + + Log.d("## TEST", "created space $spaceId ${Thread.currentThread()}") + val syncedSpace = session.spaceService().getSpace(spaceId) + + val children = listOf("General" to true /*canonical*/, "Random" to false) + + val roomIdList = children.map { + commonTestHelper.doSync { cb -> + session.createRoom(CreateRoomParams().apply { name = it.first }, cb) + } to it.second + } + + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + runBlocking { + roomIdList.forEach { entry -> + syncedSpace!!.addChildren(entry.first, viaServers, null, true) + } + } + + runBlocking { + roomIdList.forEach { + session.spaceService().setSpaceParent(it.first, spaceId, it.second, viaServers) + } + delay(400) + } + + roomIdList.forEach { + val parents = session.getRoom(it.first)?.roomSummary()?.spaceParents + val canonicalParents = session.getRoom(it.first)?.roomSummary()?.spaceParents?.filter { it.canonical == true } + + assertNotNull(parents) + assertEquals(1, parents.size, "Unexpected number of parent") + assertEquals(spaceName, parents.first().roomSummary?.name, "Unexpected parent name ") + assertEquals(if (it.second) 1 else 0, canonicalParents?.size ?: 0, "Parent of ${it.first} should be canonical ${it.second}") + } + } + + @Test + fun testFilteringBySpace() { + val session = commonTestHelper.createAccount("John", SessionTestParams(true)) + + val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + )) + + val spaceBInfo = createPublicSpace(session, "SpaceB", listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + )) + + val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + )) + + // add C as a subspace of A + val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + runBlocking { + spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) + } + + // Create orphan rooms + + val orphan1 = commonTestHelper.doSync { cb -> + session.createRoom(CreateRoomParams().apply { name = "O1" }, cb) + } + val orphan2 = commonTestHelper.doSync { cb -> + session.createRoom(CreateRoomParams().apply { name = "O2" }, cb) + } + + val allRooms = session.getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) }) + + assertEquals(9, allRooms.size, "Unexpected number of rooms") + + val orphans = session.getFlattenRoomSummaryChildOf(null) + + assertEquals(2, orphans.size, "Unexpected number of orphan rooms") + assertTrue(orphans.indexOfFirst { it.roomId == orphan1 } != -1, "O1 should be an orphan") + assertTrue(orphans.indexOfFirst { it.roomId == orphan2 } != -1, "O2 should be an orphan ${orphans.map { it.name }}") + + val aChildren = session.getFlattenRoomSummaryChildOf(spaceAInfo.spaceId) + + assertEquals(4, aChildren.size, "Unexpected number of flatten child rooms") + assertTrue(aChildren.indexOfFirst { it.name == "A1" } != -1, "A1 should be a child of A") + assertTrue(aChildren.indexOfFirst { it.name == "A2" } != -1, "A2 should be a child of A") + assertTrue(aChildren.indexOfFirst { it.name == "C1" } != -1, "CA should be a grand child of A") + assertTrue(aChildren.indexOfFirst { it.name == "C2" } != -1, "A1 should be a grand child of A") + + // Add a non canonical child and check that it does not appear as orphan + val a3 = commonTestHelper.doSync { cb -> + session.createRoom(CreateRoomParams().apply { name = "A3" }, cb) + } + runBlocking { + spaceA!!.addChildren(a3, viaServers, null, false) + delay(400) + // here we do not set the parent!! + } + + val orphansUpdate = session.getFlattenRoomSummaryChildOf(null) + assertEquals(2, orphansUpdate.size, "Unexpected number of orphan rooms ${orphansUpdate.map { it.name }}") + } + + @Test + fun testBreakCycle() { + val session = commonTestHelper.createAccount("John", SessionTestParams(true)) + + val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + )) + + val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + )) + + // add C as a subspace of A + val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + runBlocking { + spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) + } + + // add back A as subspace of C + runBlocking { + val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId) + spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true) + } + + Thread.sleep(1000) + + // A -> C -> A + + val aChildren = session.getFlattenRoomSummaryChildOf(spaceAInfo.spaceId) + + assertEquals(4, aChildren.size, "Unexpected number of flatten child rooms ${aChildren.map { it.name }}") + assertTrue(aChildren.indexOfFirst { it.name == "A1" } != -1, "A1 should be a child of A") + assertTrue(aChildren.indexOfFirst { it.name == "A2" } != -1, "A2 should be a child of A") + assertTrue(aChildren.indexOfFirst { it.name == "C1" } != -1, "CA should be a grand child of A") + assertTrue(aChildren.indexOfFirst { it.name == "C2" } != -1, "A1 should be a grand child of A") + } + + @Test + fun testLiveFlatChildren() { + val session = commonTestHelper.createAccount("John", SessionTestParams(true)) + + val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + )) + + val spaceBInfo = createPublicSpace(session, "SpaceB", listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + )) + + // add B as a subspace of A + val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + runBlocking { + spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers) + } + + val flatAChildren = runBlocking(Dispatchers.Main) { + session.getFlattenRoomSummaryChildOfLive(spaceAInfo.spaceId) + } + + commonTestHelper.waitWithLatch { latch -> + + val childObserver = object : Observer> { + override fun onChanged(children: List?) { +// Log.d("## TEST", "Space A flat children update : ${children?.map { it.name }}") + System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}") + if (children?.indexOfFirst { it.name == "C1" } != -1 + && children?.indexOfFirst { it.name == "C2" } != -1 + ) { + // B1 has been added live! + latch.countDown() + flatAChildren.removeObserver(this) + } + } + } + + val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + )) + + // add C as subspace of B + runBlocking { + val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) + spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + } + + // C1 and C2 should be in flatten child of A now + + GlobalScope.launch(Dispatchers.Main) { flatAChildren.observeForever(childObserver) } + } + + // Test part one of the rooms + + val bRoomId = spaceBInfo.roomIds.first() + val bRoom = session.getRoom(bRoomId) + + commonTestHelper.waitWithLatch { latch -> + + val childObserver = object : Observer> { + override fun onChanged(children: List?) { + System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}") + if (children?.any { it.roomId == bRoomId } == false) { + // B1 has been added live! + latch.countDown() + flatAChildren.removeObserver(this) + } + } + } + + // part from b room + commonTestHelper.doSync { + bRoom!!.leave(null, it) + } + // The room should have disapear from flat children + GlobalScope.launch(Dispatchers.Main) { flatAChildren.observeForever(childObserver) } + } + } + + data class TestSpaceCreationResult( + val spaceId: String, + val roomIds: List + ) + + private fun createPublicSpace(session: Session, + spaceName: String, + childInfo: List> + /** Name, auto-join, canonical*/ + ): TestSpaceCreationResult { + val spaceId = runBlocking { + session.spaceService().createSpace(spaceName, "Test Topic", null, true) + } + + val syncedSpace = session.spaceService().getSpace(spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + val roomIds = + childInfo.map { entry -> + commonTestHelper.doSync { cb -> + session.createRoom(CreateRoomParams().apply { name = entry.first }, cb) + } + } + + roomIds.forEachIndexed { index, roomId -> + runBlocking { + syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) + val canonical = childInfo[index].third + if (canonical != null) { + session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) + } + } + } + return TestSpaceCreationResult(spaceId, roomIds) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 257c83564e..5f2bc716f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -34,6 +34,8 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService import org.matrix.android.sdk.api.session.search.SearchResult +import org.matrix.android.sdk.api.session.space.Space +import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional /** @@ -90,5 +92,6 @@ interface Room : limit: Int, beforeLimit: Int, afterLimit: Int, +// fun getSpaceParents(): List includeProfile: Boolean): SearchResult } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index 22045366cb..5296ad58b0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData import androidx.paging.PagedList import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams @@ -197,4 +198,8 @@ interface RoomService { .setEnablePlaceholders(false) .setPrefetchDistance(10) .build() + + fun getFlattenRoomSummaryChildOf(spaceId: String?, memberships: List = Membership.activeMemberships()) : List + + fun getFlattenRoomSummaryChildOfLive(spaceId: String?, memberships: List = Membership.activeMemberships()): LiveData> } 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 c8d52302e9..47618e31c9 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 @@ -21,11 +21,27 @@ 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 +import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams { return RoomSummaryQueryParams.Builder().apply(init).build() } +fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): SpaceSummaryQueryParams { + return RoomSummaryQueryParams.Builder() + .apply(init) + .apply { + this.includeType = listOf(RoomType.SPACE) + this.excludeType = null + this.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + }.build() +} + +enum class RoomCategoryFilter { + ONLY_DM, + ONLY_ROOMS, + ALL +} /** * This class can be used to filter room summaries to use with: * [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService] @@ -37,7 +53,8 @@ data class RoomSummaryQueryParams( val memberships: List, val roomCategoryFilter: RoomCategoryFilter?, val roomTagQueryFilter: RoomTagQueryFilter? - val excludeType: List + val excludeType: List?, + val includeType: List? ) { class Builder { @@ -49,6 +66,7 @@ data class RoomSummaryQueryParams( var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL var roomTagQueryFilter: RoomTagQueryFilter? = null var excludeType: List = listOf(RoomType.SPACE) + var includeType: List? = null fun build() = RoomSummaryQueryParams( roomId = roomId, @@ -56,8 +74,9 @@ data class RoomSummaryQueryParams( canonicalAlias = canonicalAlias, memberships = memberships, roomCategoryFilter = roomCategoryFilter, - roomTagQueryFilter = roomTagQueryFilter - excludeType = excludeType + roomTagQueryFilter = roomTagQueryFilter, + excludeType = excludeType, + includeType = includeType ) } } 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 ac87a16911..f08f605a24 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,18 +27,18 @@ 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( - override val roomId: String, + val roomId: String, // Computed display name - 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 displayName: String = "", + val name: String = "", + val topic: String = "", + val avatarUrl: String = "", + val canonicalAlias: String? = null, + val aliases: List = emptyList(), + val joinedMembersCount: Int? = 0, + val invitedMembersCount: Int? = 0, val latestPreviewableEvent: TimelineEvent? = null, - override val otherMemberIds: List = emptyList(), + val otherMemberIds: List = emptyList(), val isDirect: Boolean = false, val notificationCount: Int = 0, val highlightCount: Int = 0, @@ -55,8 +55,10 @@ data class RoomSummary constructor( val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null, val hasFailedSending: Boolean = false, - override val roomType: String? = null -) : IRoomSummary { + val roomType: String? = null, + val spaceParents: List? = null, + val children: List? = null +) { val isVersioned: Boolean get() = versioningState != VersioningState.NONE diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt index 38d9f1e74e..1302a34daa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt @@ -17,8 +17,16 @@ package org.matrix.android.sdk.api.session.room.model data class SpaceChildInfo( - val roomSummary: IRoomSummary?, + val childRoomId: String, + // We might not know this child at all, + // i.e we just know it exists but no info on type/name/etc.. + val isKnown: Boolean, + val roomType: String?, + val name: String?, + val topic: String?, + val avatarUrl: String?, val order: String?, + val activeMemberCount: Int?, val autoJoin: Boolean, val viaServers: List ) 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/SpaceParentInfo.kt similarity index 61% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/IRoomSummary.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceParentInfo.kt index 1724f00c99..5ed81b0646 100644 --- 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/SpaceParentInfo.kt @@ -5,7 +5,7 @@ * 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 + * 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, @@ -16,16 +16,9 @@ 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? -} +data class SpaceParentInfo( + val parentId: String?, + val roomSummary: RoomSummary?, + val canonical: Boolean?, + val viaServers: List +) 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 index 0172b3701b..cfbffc128c 100644 --- 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 @@ -24,9 +24,9 @@ interface Space { fun asRoom() : Room /** - * A current snapshot of [RoomSummary] associated with the room + * A current snapshot of [RoomSummary] associated with the space */ - fun spaceSummary(): SpaceSummary? + fun spaceSummary(): RoomSummary? suspend fun addChildren(roomId: String, viaServers: List, order: String?, autoJoin: Boolean = false) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt index 3e30f14748..15b6f7d852 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -60,9 +60,9 @@ interface SpaceService { * 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 getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> - fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List + fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List sealed class JoinSpaceResult { object Success : JoinSpaceResult() @@ -79,4 +79,14 @@ interface SpaceService { viaServers: List = emptyList()): JoinSpaceResult suspend fun rejectInvite(spaceId: String, reason: String?) + +// fun getSpaceParentsOfRoom(roomId: String) : List + + /** + * Let this room declare that it has a parent. + * @param canonical true if it should be the main parent of this room + * In practice, well behaved rooms should only have one canonical parent, but given this is not enforced: + * if multiple are present the client should select the one with the lowest room ID, as determined via a lexicographic utf-8 ordering. + */ + suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: 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 deleted file mode 100644 index 1473ed7a96..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceSummary.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 -import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo - -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/SpaceParentContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt index b3f7267580..871a494914 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt @@ -26,7 +26,6 @@ import com.squareup.moshi.JsonClass * "state_key": "!space:example.com", * "content": { * "via": ["example.com"], - * "present": true, * "canonical": true, * } * } @@ -38,11 +37,6 @@ data class SpaceParentContent( * Parents where via is not present are ignored. */ @Json(name = "via") val via: List? = null, - /** - * present: true key is included to distinguish from a deleted state event - * Parent where present is not present (sic) or is not set to true are ignored. - */ - @Json(name = "present") val present: Boolean? = false, /** * Canonical determines whether this is the main parent for the space. * When a user joins a room with a canonical parent, clients may switch to view the room 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 a792248764..a904b43faa 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,7 +22,6 @@ 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 @@ -152,8 +151,6 @@ 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 b14ed9e9c7..9dd8cda8b9 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 @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.database import io.realm.DynamicRealm -import io.realm.FieldAttribute import io.realm.RealmMigration import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields @@ -31,8 +30,8 @@ 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.SpaceChildInfoEntityFields -import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields import timber.log.Timber import javax.inject.Inject @@ -200,21 +199,34 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { fun migrateTo10(realm: DynamicRealm) { Timber.d("Step 9 -> 10") + realm.schema.create("SpaceChildSummaryEntity") + ?.addField(SpaceChildSummaryEntityFields.ORDER, String::class.java) + ?.addField(SpaceChildSummaryEntityFields.CHILD_ROOM_ID, String::class.java) + ?.addField(SpaceChildSummaryEntityFields.AUTO_JOIN, Boolean::class.java) + ?.setNullable(SpaceChildSummaryEntityFields.AUTO_JOIN, true) + ?.addRealmObjectField(SpaceChildSummaryEntityFields.CHILD_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) + +// realm.schema.create("SpaceSummaryEntity") +// ?.addField(SpaceSummaryEntityFields.SPACE_ID, String::class.java, FieldAttribute.PRIMARY_KEY) +// ?.setRequired(SpaceSummaryEntityFields.SPACE_ID, true) +// ?.addRealmObjectField(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) +// ?.addRealmListField(SpaceSummaryEntityFields.CHILDREN.`$`, spaceChildInfoSchema!!) + + realm.schema.create("SpaceParentSummaryEntity") + ?.addField(SpaceParentSummaryEntityFields.PARENT_ROOM_ID, String::class.java) + ?.addField(SpaceParentSummaryEntityFields.CANONICAL, Boolean::class.java) + ?.setNullable(SpaceParentSummaryEntityFields.CANONICAL, true) +// ?.addRealmListField(RoomParentRelationInfoEntityFields.VIA_SERVERS.`$`, String::class.java) +// ?.addRealmObjectField(RoomParentRelationInfoEntityFields.SPACE_SUMMARY_ENTITY.`$`, realm.schema.get("SpaceSummaryEntity")!!) + ?.addRealmObjectField(SpaceParentSummaryEntityFields.PARENT_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) + 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) } - - val spaceChildInfoSchema = realm.schema.create("SpaceChildInfoEntity") - ?.addField(SpaceChildInfoEntityFields.ORDER, String::class.java) - ?.addRealmListField(SpaceChildInfoEntityFields.VIA_SERVERS.`$`, String::class.java) - ?.addRealmObjectField(SpaceChildInfoEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) - - 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.`$`, spaceChildInfoSchema!!) + ?.addRealmListField(RoomSummaryEntityFields.PARENTS.`$`, realm.schema.get("SpaceParentSummaryEntity")!!) + ?.addRealmListField(RoomSummaryEntityFields.CHILDREN.`$`, realm.schema.get("SpaceChildSummaryEntity")!!) } } 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 c74eb4460d..410500ed8e 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 @@ -17,6 +17,8 @@ package org.matrix.android.sdk.internal.database.mapper import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker @@ -64,7 +66,29 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, inviterId = roomSummaryEntity.inviterId, hasFailedSending = roomSummaryEntity.hasFailedSending, - roomType = roomSummaryEntity.roomType + roomType = roomSummaryEntity.roomType, + spaceParents = roomSummaryEntity.parents.map { relationInfoEntity -> + SpaceParentInfo( + parentId = relationInfoEntity.parentRoomId, + roomSummary = relationInfoEntity.parentSummaryEntity?.let { map(it) }, + canonical = relationInfoEntity.canonical ?: false, + viaServers = relationInfoEntity.viaServers.toList() + ) + }, + children = roomSummaryEntity.children.map { + SpaceChildInfo( + childRoomId = it.childRoomId ?: "", + isKnown = it.childSummaryEntity != null, + roomType = it.childSummaryEntity?.roomType, + name = it.childSummaryEntity?.name, + topic = it.childSummaryEntity?.topic, + avatarUrl = it.childSummaryEntity?.avatarUrl, + activeMemberCount = it.childSummaryEntity?.joinedMembersCount, + order = it.order, + autoJoin = it.autoJoin ?: false, + viaServers = it.viaServers.toList() + ) + } ) } } 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 deleted file mode 100644 index d08528598d..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/SpaceSummaryMapper.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.room.model.SpaceChildInfo -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 { - SpaceChildInfo( - roomSummary = it.roomSummaryEntity?.let { rs -> roomSummaryMapper.map(rs) }, - autoJoin = it.autoJoin ?: false, - viaServers = it.viaServers.map { it }, - order = it.order - ) - } - ) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt index 3ff2532604..58297776f0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt @@ -43,6 +43,5 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "", set(value) { membersLoadStatusStr = value.name } - companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt index c87ac15a78..85e4595da5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt @@ -28,6 +28,9 @@ import org.matrix.android.sdk.api.session.room.model.tag.RoomTag internal open class RoomSummaryEntity( @PrimaryKey var roomId: String = "" + var roomType: String? = null, + var parents: RealmList = RealmList(), + var children: RealmList = RealmList() ) : RealmObject() { var displayName: String? = "" @@ -244,6 +247,5 @@ internal open class RoomSummaryEntity( roomEncryptionTrustLevelStr = value?.name } } - companion object } 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 76116be1a8..72ae512fa5 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 @@ -62,7 +62,7 @@ import io.realm.annotations.RealmModule UserAccountDataEntity::class, ScalarTokenEntity::class, WellknownIntegrationManagerConfigEntity::class, - SpaceSummaryEntity::class, - SpaceChildInfoEntity::class + SpaceChildSummaryEntity::class, + SpaceParentSummaryEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt similarity index 69% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt index 7862207901..982c9ece6a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt @@ -22,14 +22,22 @@ import io.realm.RealmObject /** * Decorates room summary with space related information. */ -internal open class SpaceChildInfoEntity( - var viaServers: RealmList = RealmList(), - // Use for alphabetic ordering of this child +internal open class SpaceChildSummaryEntity( +// var isSpace: Boolean = false, + var order: String? = null, - // If true, this child should be join when parent is joined + var autoJoin: Boolean? = null, - // link to the actual room (check type to see if it's a subspace) - var roomSummaryEntity: RoomSummaryEntity? = null + + var childRoomId: String? = null, + // Link to the actual space summary if it is known locally + var childSummaryEntity: RoomSummaryEntity? = null, + + var viaServers: RealmList = RealmList() +// var owner: RoomSummaryEntity? = null, + +// var level: Int = 0 + ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt new file mode 100644 index 0000000000..af32cd2b83 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt @@ -0,0 +1,49 @@ +/* + * 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 + +/** + * Decorates room summary with space related information. + */ +internal open class SpaceParentSummaryEntity( + /** + * Determines whether this is the main parent for the space + * When a user joins a room with a canonical parent, clients may switch to view the room in the context of that space, + * peeking into it in order to find other rooms and group them together. + * In practice, well behaved rooms should only have one canonical parent, but given this is not enforced: + * if multiple are present the client should select the one with the lowest room ID, + * as determined via a lexicographic utf-8 ordering. + */ + var canonical: Boolean? = null, + + var parentRoomId: String? = null, + // Link to the actual space summary if it is known locally + var parentSummaryEntity: RoomSummaryEntity? = null, + + var viaServers: RealmList = RealmList() + +// var child: RoomSummaryEntity? = null, + +// var level: Int = 0 + +) : RealmObject() { + + companion object +} 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 deleted file mode 100644 index e63b5b9d55..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceSummaryEntity.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 deleted file mode 100644 index b6403c596f..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/SpaceSummaryEntityQueries.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 1d8eb6c95e..1c22faa7b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.alias.AliasService import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.members.MembershipService import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService import org.matrix.android.sdk.api.session.room.read.ReadService @@ -36,11 +37,16 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService import org.matrix.android.sdk.api.session.search.SearchResult +import org.matrix.android.sdk.api.session.space.Space +import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.search.SearchTask +import org.matrix.android.sdk.internal.session.space.DefaultSpace +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.awaitCallback import java.security.InvalidParameterException import javax.inject.Inject @@ -148,4 +154,9 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, ) ) } + + override fun asSpace(): Space? { + if (roomSummary()?.roomType != RoomType.SPACE) return null + return DefaultSpace(this, roomSummaryDataSource) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 4724167e87..cd39e633dd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams @@ -33,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase @@ -63,7 +65,9 @@ internal class DefaultRoomService @Inject constructor( private val peekRoomTask: PeekRoomTask, private val roomGetter: RoomGetter, private val roomSummaryDataSource: RoomSummaryDataSource, - private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource + private val roomSummaryMapper: RoomSummaryMapper, + private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, + private val taskExecutor: TaskExecutor ) : RoomService { override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable { @@ -168,4 +172,18 @@ internal class DefaultRoomService @Inject constructor( override suspend fun peekRoom(roomIdOrAlias: String): PeekResult { return peekRoomTask.execute(PeekRoomTask.Params(roomIdOrAlias)) } + + override fun getFlattenRoomSummaryChildOf(spaceId: String?, memberships: List): List { + if (spaceId == null) { + return roomSummaryDataSource.getFlattenOrphanRooms() + } + return roomSummaryDataSource.getAllRoomSummaryChildOf(spaceId, memberships) + } + + override fun getFlattenRoomSummaryChildOfLive(spaceId: String?, memberships: List): LiveData> { + if (spaceId == null) { + return roomSummaryDataSource.getFlattenOrphanRoomsLive() + } + return roomSummaryDataSource.getAllRoomSummaryChildOfLive(spaceId, memberships) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt index f440a67710..0f64bb60ad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt @@ -18,8 +18,8 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.space.Space +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.space.DefaultSpace -import org.matrix.android.sdk.internal.session.space.SpaceSummaryDataSource import javax.inject.Inject internal interface SpaceGetter { @@ -28,7 +28,7 @@ internal interface SpaceGetter { internal class DefaultSpaceGetter @Inject constructor( private val roomGetter: RoomGetter, - private val spaceSummaryDataSource: SpaceSummaryDataSource + private val spaceSummaryDataSource: RoomSummaryDataSource ) : SpaceGetter { override fun get(spaceId: String): Space? { 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/RoomChildRelationInfo.kt similarity index 64% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomRelationshipHelper.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt index c04f5d3948..06ab21d8db 100644 --- 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/RoomChildRelationInfo.kt @@ -20,6 +20,7 @@ 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.api.session.space.model.SpaceParentContent 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 @@ -35,8 +36,8 @@ import timber.log.Timber * * - Separately, rooms can claim parents via the m.room.parent state event: */ -internal class RoomRelationshipHelper(private val realm: Realm, - private val roomId: String +internal class RoomChildRelationInfo(private val realm: Realm, + private val roomId: String ) { data class SpaceChildInfo( @@ -46,15 +47,24 @@ internal class RoomRelationshipHelper(private val realm: Realm, val viaServers: List ) + data class SpaceParentInfo( + val roomId: String, + val canonical: Boolean, + val viaServers: List, + val stateEventSender: String + ) + /** * Gets the ordered list of valid child description. */ fun getDirectChildrenDescriptions(): List { return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD) - .findAll() + .findAll().also { + Timber.v("## Space: Found ${it.count()} m.space.child state events for $roomId") + } .mapNotNull { ContentMapper.map(it.root?.content).toModel()?.let { scc -> - Timber.d("## Space child desc state event $scc") + Timber.v("## Space child desc state event $scc") // Children where via is not present are ignored. scc.via?.let { via -> SpaceChildInfo( @@ -68,4 +78,25 @@ internal class RoomRelationshipHelper(private val realm: Realm, } .sortedBy { it.order } } + + fun getParentDescriptions(): List { + return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_PARENT) + .findAll().also { + Timber.v("## Space: Found ${it.count()} m.space.parent state events for $roomId") + } + .mapNotNull { + ContentMapper.map(it.root?.content).toModel()?.let { scc -> + Timber.v("## Space parent desc state event $scc") + // Parent where via is not present are ignored. + scc.via?.let { via -> + SpaceParentInfo( + roomId = it.stateKey, + canonical = scc.canonical ?: false, + viaServers = via, + stateEventSender = it.root?.sender ?: "" + ) + } + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/HierarchyLiveDataHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/HierarchyLiveDataHelper.kt new file mode 100644 index 0000000000..ced1ad963e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/HierarchyLiveDataHelper.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.summary + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +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.util.Optional + +internal class HierarchyLiveDataHelper( + val spaceId: String, + val memberships: List, + val roomSummaryDataSource: RoomSummaryDataSource) { + + private val sources = HashMap>>() + private val mediatorLiveData = MediatorLiveData>() + + fun liveData() = mediatorLiveData + + init { + onChange() + } + + private fun parentsToCheck(): List { + val spaces = ArrayList() + roomSummaryDataSource.getSpaceSummary(spaceId)?.let { + roomSummaryDataSource.flattenSubSpace(it, emptyList(), spaces, memberships) + } + return spaces + } + + private fun onChange() { + val existingSources = sources.keys.toList() + val newSources = parentsToCheck().map { it.roomId } + val addedSources = newSources.filter { !existingSources.contains(it) } + val removedSource = existingSources.filter { !newSources.contains(it) } + addedSources.forEach { + val liveData = roomSummaryDataSource.getSpaceSummaryLive(it) + mediatorLiveData.addSource(liveData) { onChange() } + sources[it] = liveData + } + + removedSource.forEach { + sources[it]?.let { mediatorLiveData.removeSource(it) } + } + + sources[spaceId]?.value?.getOrNull()?.let { spaceSummary -> + val results = ArrayList() + roomSummaryDataSource.flattenChild(spaceSummary, emptyList(), results, memberships) + mediatorLiveData.postValue(results.map { 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 576e7f4eba..2468661ada 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 @@ -1,5 +1,6 @@ /* * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,9 +28,17 @@ import io.realm.Sort import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult +import io.realm.kotlin.where +import org.matrix.android.sdk.api.session.room.RoomCategoryFilter +import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams +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.RoomSummaryMapper @@ -84,6 +93,36 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat ) } + fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> { + return getRoomSummariesLive(queryParams) + } + + fun getSpaceSummary(roomIdOrAlias: String): RoomSummary? { + return getRoomSummary(roomIdOrAlias).let { + it?.takeIf { it.roomType == RoomType.SPACE } + } + } + + fun getSpaceSummaryLive(roomId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> + RoomSummaryEntity.where(realm, roomId) + .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) + .equalTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE) + }, + { + roomSummaryMapper.map(it) + } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + + fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List { + return getRoomSummaries(spaceSummaryQueryParams) + } + fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List { return monarchy.fetchAllMappedSync( { breadcrumbsQuery(it, queryParams) }, @@ -190,9 +229,134 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat } } - queryParams.excludeType.forEach { + queryParams.excludeType?.forEach { query.notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, it) } + queryParams.includeType?.forEach { + query.equalTo(RoomSummaryEntityFields.ROOM_TYPE, it) + } + queryParams.roomCategoryFilter?.let { + when (it) { + RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false) + RoomCategoryFilter.ALL -> { + // nop + } + } + } return query } + + fun getAllRoomSummaryChildOf(spaceAliasOrId: String, memberShips: List): List { + val space = getSpaceSummary(spaceAliasOrId) ?: return emptyList() + val result = ArrayList() + flattenChild(space, emptyList(), result, memberShips) + return result + } + + fun getAllRoomSummaryChildOfLive(spaceId: String, memberShips: List): LiveData> { + // we want to listen to all spaces in hierarchy and on change compute back all childs + // and switch map to listen thoose? + val mediatorLiveData = HierarchyLiveDataHelper(spaceId, memberShips, this).liveData() + + return Transformations.switchMap(mediatorLiveData) { allIds -> + monarchy.findAllMappedWithChanges( + { + it.where() + .`in`(RoomSummaryEntityFields.ROOM_ID, allIds.toTypedArray()) + .`in`(RoomSummaryEntityFields.MEMBERSHIP_STR, memberShips.map { it.name }.toTypedArray()) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, false) + }, + { + roomSummaryMapper.map(it) + }) + } + } + + fun getFlattenOrphanRooms(): List { + return getRoomSummaries(roomSummaryQueryParams { + memberships = Membership.activeMemberships() + excludeType = listOf(RoomType.SPACE) + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + }).filter { + // we need to check if orphan + isOrphan(it) + } + } + + fun getFlattenOrphanRoomsLive(): LiveData> { + return Transformations.map( + getRoomSummariesLive(roomSummaryQueryParams { + memberships = Membership.activeMemberships() + excludeType = listOf(RoomType.SPACE) + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + })) { + it.filter { + isOrphan(it) + } + } + } + + private fun isOrphan(roomSummary: RoomSummary): Boolean { + if (roomSummary.roomType == RoomType.SPACE && roomSummary.membership.isActive()) { + return false + } + // all parents line should be orphan + roomSummary.spaceParents?.forEach { info -> + if (info.roomSummary != null && !info.roomSummary.membership.isLeft()) { + if (!isOrphan(info.roomSummary)) { + return false + } + } + } + + // it may not have a parent relation but could be a child of some other.... + for (spaceSummary in getSpaceSummaries(spaceSummaryQueryParams { memberships = Membership.activeMemberships() })) { + if (spaceSummary.children?.any { it.childRoomId == roomSummary.roomId } == true) { + return false + } + } + + return true + } + + fun flattenChild(current: RoomSummary, parenting: List, output: MutableList, memberShips: List) { + current.children?.sortedBy { it.order ?: it.name }?.forEach { childInfo -> + if (childInfo.roomType == RoomType.SPACE) { + // Add recursive + if (!parenting.contains(childInfo.childRoomId)) { // avoid cycles! + getSpaceSummary(childInfo.childRoomId)?.let { subSpace -> + if (memberShips.isEmpty() || memberShips.contains(subSpace.membership)) { + flattenChild(subSpace, parenting + listOf(current.roomId), output, memberShips) + } + } + } + } else if (childInfo.isKnown) { + getRoomSummary(childInfo.childRoomId)?.let { + if (memberShips.isEmpty() || memberShips.contains(it.membership)) { + if (!it.isDirect) { + output.add(it) + } + } + } + } + } + } + + fun flattenSubSpace(current: RoomSummary, parenting: List, output: MutableList, memberShips: List) { + output.add(current) + current.children?.sortedBy { it.order ?: it.name }?.forEach { + if (it.roomType == RoomType.SPACE) { + // Add recursive + if (!parenting.contains(it.childRoomId)) { // avoid cycles! + getSpaceSummary(it.childRoomId)?.let { subSpace -> + if (memberShips.isEmpty() || memberShips.contains(subSpace.membership)) { + output.add(subSpace) + flattenSubSpace(subSpace, parenting + listOf(current.roomId), output, memberShips) + } + } + } + } + } + } } 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 3d01021811..e7cb9688dd 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 @@ -39,19 +39,22 @@ 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.SpaceChildInfoEntity -import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity +import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntity +import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntity 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 import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.isEventRead +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereType import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.extensions.clearWith 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.room.relationship.RoomChildRelationInfo +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications import timber.log.Timber @@ -62,7 +65,8 @@ internal class RoomSummaryUpdater @Inject constructor( private val roomDisplayNameResolver: RoomDisplayNameResolver, private val roomAvatarResolver: RoomAvatarResolver, private val eventDecryptor: EventDecryptor, - private val crossSigningService: DefaultCrossSigningService) { + private val crossSigningService: DefaultCrossSigningService, + private val stateEventDataSource: StateEventDataSource) { fun update(realm: Realm, roomId: String, @@ -163,28 +167,6 @@ internal class RoomSummaryUpdater @Inject constructor( crossSigningService.onUsersDeviceUpdate(otherRoomMembers) } } - - if (roomType == RoomType.SPACE) { - Timber.v("## Space: Updating summary for Space $roomId membership: ${roomSummaryEntity.membership}") - val spaceSummaryEntity = SpaceSummaryEntity() - spaceSummaryEntity.spaceId = roomId - spaceSummaryEntity.roomSummaryEntity = roomSummaryEntity - spaceSummaryEntity.children.clear() - spaceSummaryEntity.children.addAll( - RoomRelationshipHelper(realm, roomId).getDirectChildrenDescriptions() - .map { - Timber.v("## Space: Updating summary for room $roomId with info $it") - realm.createObject().apply { - this.roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, it.roomId) - this.order = it.order - this.autoJoin = it.autoJoin - }.also { - Timber.v("## Space: Updating summary for room $roomId with children $it") - } - } - ) - realm.insertOrUpdate(spaceSummaryEntity) - } } private fun RoomSummaryEntity.updateHasFailedSending() { @@ -196,4 +178,55 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.updateHasFailedSending() roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) } + + /** + * Should be called at the end of the room sync, to check and validate all parent/child relations + */ + fun validateSpaceRelationship(realm: Realm) { + // Do level 0 stuffs + + realm.where(RoomSummaryEntity::class.java).findAll().forEach { roomSummary -> + if (roomSummary.roomType == RoomType.SPACE) { + roomSummary.children.clearWith { it.deleteFromRealm() } + roomSummary.children.addAll( + RoomChildRelationInfo(realm, roomSummary.roomId).getDirectChildrenDescriptions() + .map { + Timber.v("## Space: Updating summary for room ${roomSummary.roomId} with info $it") + realm.createObject().apply { + this.childRoomId = it.roomId + this.childSummaryEntity = RoomSummaryEntity.where(realm, it.roomId).findFirst() + this.order = it.order + this.autoJoin = it.autoJoin + this.viaServers.addAll(it.viaServers) +// this.level = 0 + }.also { + Timber.v("## Space: Updating summary for room ${roomSummary.roomId} with children $it") + } + } + ) + } + + // check parents + roomSummary.parents.clearWith { it.deleteFromRealm() } + roomSummary.parents.addAll( + RoomChildRelationInfo(realm, roomSummary.roomId).getParentDescriptions() + .map { parentInfo -> + Timber.v("## Space: Updating summary for room ${roomSummary.roomId} with parent info $parentInfo") + realm.createObject().apply { + this.parentRoomId = parentInfo.roomId + this.parentSummaryEntity = RoomSummaryEntity.where(realm, parentInfo.roomId).findFirst() + this.canonical = parentInfo.canonical + this.viaServers.addAll(parentInfo.viaServers) +// this.level = 0 + }.also { + Timber.v("## Space: Updating summary for room ${roomSummary.roomId} with parent $it") + } + } + ) + } + } + +// private fun isValidCanonical() : Boolean { +// +// } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/CreateSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/CreateSpaceTask.kt index 5f174587d0..826fa76f77 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/CreateSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/CreateSpaceTask.kt @@ -21,8 +21,8 @@ import kotlinx.coroutines.TimeoutCancellationException import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.internal.database.awaitNotEmptyResult -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.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.task.Task @@ -44,8 +44,8 @@ internal class DefaultCreateSpaceTask @Inject constructor( try { awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> - realm.where(SpaceSummaryEntity::class.java) - .equalTo(SpaceSummaryEntityFields.SPACE_ID, spaceId) + realm.where(RoomSummaryEntity::class.java) + .equalTo(RoomSummaryEntityFields.ROOM_ID, spaceId) } } catch (exception: TimeoutCancellationException) { throw CreateRoomFailure.CreatedWithTimeout(spaceId) 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 index efba103ab7..13b9465d58 100644 --- 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 @@ -21,17 +21,18 @@ 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.events.model.toModel import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.space.Space -import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.api.session.space.model.SpaceChildContent +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource -internal class DefaultSpace(private val room: Room, private val spaceSummaryDataSource: SpaceSummaryDataSource) : Space { +internal class DefaultSpace(private val room: Room, private val spaceSummaryDataSource: RoomSummaryDataSource) : Space { override fun asRoom(): Room { return room } - override fun spaceSummary(): SpaceSummary? { + override fun spaceSummary(): RoomSummary? { return spaceSummaryDataSource.getSpaceSummary(asRoom().roomId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index 0210c81e5d..cdd2673fde 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -19,26 +19,37 @@ package org.matrix.android.sdk.internal.session.space import android.net.Uri import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.query.QueryStringValue 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.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper 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.api.session.space.model.SpaceChildContent +import org.matrix.android.sdk.api.session.space.model.SpaceParentContent import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.RoomGetter import org.matrix.android.sdk.internal.session.room.SpaceGetter import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult +import java.lang.IllegalArgumentException import javax.inject.Inject internal class DefaultSpaceService @Inject constructor( @SessionDatabase private val monarchy: Monarchy, + @UserId private val userId: String, private val createSpaceTask: CreateSpaceTask, // private val joinRoomTask: JoinRoomTask, private val joinSpaceTask: JoinSpaceTask, @@ -47,8 +58,9 @@ internal class DefaultSpaceService @Inject constructor( // private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, // private val roomIdByAliasTask: GetRoomIdByAliasTask, // private val deleteRoomAliasTask: DeleteRoomAliasTask, -// private val roomGetter: RoomGetter, - private val spaceSummaryDataSource: SpaceSummaryDataSource, + private val roomGetter: RoomGetter, + private val roomSummaryDataSource: RoomSummaryDataSource, + private val stateEventDataSource: StateEventDataSource, private val peekSpaceTask: PeekSpaceTask, private val resolveSpaceInfoTask: ResolveSpaceInfoTask, private val leaveRoomTask: LeaveRoomTask @@ -73,12 +85,12 @@ internal class DefaultSpaceService @Inject constructor( return spaceGetter.get(spaceId) } - override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> { - return spaceSummaryDataSource.getRoomSummariesLive(queryParams) + override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> { + return roomSummaryDataSource.getSpaceSummariesLive(queryParams) } - override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List { - return spaceSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams) + override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List { + return roomSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams) } override suspend fun peekSpace(spaceId: String): SpacePeekResult { @@ -108,21 +120,16 @@ internal class DefaultSpaceService @Inject constructor( ?.firstOrNull { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD } ?.content.toModel() SpaceChildInfo( - roomSummary = RoomSummary( - roomId = childSummary.roomId, - roomType = childSummary.roomType, - name = childSummary.name ?: "", - displayName = childSummary.name ?: "", - topic = childSummary.topic ?: "", - joinedMembersCount = childSummary.numJoinedMembers, - avatarUrl = childSummary.avatarUrl ?: "", - encryptionEventTs = null, - typingUsers = emptyList(), - isEncrypted = false - ), + childRoomId = childSummary.roomId, + isKnown = true, + roomType = childSummary.roomType, + name = childSummary.name, + topic = childSummary.topic, + avatarUrl = childSummary.avatarUrl, order = childStateEv?.order, autoJoin = childStateEv?.autoJoin ?: false, - viaServers = childStateEv?.via ?: emptyList() + viaServers = childStateEv?.via ?: emptyList(), + activeMemberCount = childSummary.numJoinedMembers ) } ?: emptyList() ) @@ -138,4 +145,42 @@ internal class DefaultSpaceService @Inject constructor( override suspend fun rejectInvite(spaceId: String, reason: String?) { leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason)) } + +// override fun getSpaceParentsOfRoom(roomId: String): List { +// return spaceSummaryDataSource.getParentsOfRoom(roomId) +// } + + override suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: List) { + // Should we perform some validation here?, + // and if client want to bypass, it could use sendStateEvent directly? + if (canonical) { + // check that we can send m.child in the parent room + if (roomSummaryDataSource.getRoomSummary(parentSpaceId)?.membership != Membership.JOIN) { + throw UnsupportedOperationException("Cannot add canonical child if not member of parent") + } + val powerLevelsEvent = stateEventDataSource.getStateEvent( + roomId = parentSpaceId, + eventType = EventType.STATE_ROOM_POWER_LEVELS, + stateKey = QueryStringValue.NoCondition + ) + val powerLevelsContent = powerLevelsEvent?.content?.toModel() + ?: throw UnsupportedOperationException("Cannot add canonical child, not enough power level") + val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) + if (!powerLevelsHelper.isUserAllowedToSend(userId, true, EventType.STATE_SPACE_CHILD)) { + throw UnsupportedOperationException("Cannot add canonical child, not enough power level") + } + } + + val room = roomGetter.getRoom(childRoomId) + ?: throw IllegalArgumentException("Unknown Room $childRoomId") + + room.sendStateEvent( + eventType = EventType.STATE_SPACE_PARENT, + stateKey = parentSpaceId, + body = SpaceParentContent( + via = viaServers, + canonical = canonical + ).toContent() + ) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt index 6ee3652761..9379c0d650 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt @@ -22,11 +22,12 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.internal.database.awaitNotEmptyResult -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.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.task.Task import timber.log.Timber import java.util.concurrent.TimeUnit @@ -45,7 +46,7 @@ internal class DefaultJoinSpaceTask @Inject constructor( private val joinRoomTask: JoinRoomTask, @SessionDatabase private val realmConfiguration: RealmConfiguration, - private val spaceSummaryDataSource: SpaceSummaryDataSource + private val roomSummaryDataSource: RoomSummaryDataSource ) : JoinSpaceTask { override suspend fun execute(params: JoinSpaceTask.Params): SpaceService.JoinSpaceResult { @@ -65,15 +66,15 @@ internal class DefaultJoinSpaceTask @Inject constructor( Timber.v("## Space: > Wait for post joined sync ${params.roomIdOrAlias} ...") try { awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(2L)) { realm -> - realm.where(SpaceSummaryEntity::class.java) + realm.where(RoomSummaryEntity::class.java) .apply { if (params.roomIdOrAlias.startsWith("!")) { - equalTo(SpaceSummaryEntityFields.SPACE_ID, params.roomIdOrAlias) + equalTo(RoomSummaryEntityFields.ROOM_ID, params.roomIdOrAlias) } else { - equalTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.CANONICAL_ALIAS, params.roomIdOrAlias) + equalTo(RoomSummaryEntityFields.CANONICAL_ALIAS, params.roomIdOrAlias) } } - .equalTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.MEMBERSHIP_STR, Membership.JOIN.name) + .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) } } catch (exception: TimeoutCancellationException) { Timber.w("## Space: > Error created with timeout") @@ -83,21 +84,21 @@ internal class DefaultJoinSpaceTask @Inject constructor( val errors = HashMap() Timber.v("## Space: > Sync done ...") // after that i should have the children (? do I need to paginate to get state) - val summary = spaceSummaryDataSource.getSpaceSummary(params.roomIdOrAlias) - Timber.v("## Space: Found space summary Name:[${summary?.roomSummary?.name}] children: ${summary?.children?.size}") + val summary = roomSummaryDataSource.getSpaceSummary(params.roomIdOrAlias) + Timber.v("## Space: Found space summary Name:[${summary?.name}] children: ${summary?.children?.size}") summary?.children?.forEach { - val childRoomSummary = it.roomSummary ?: return@forEach - Timber.v("## Space: Processing child :[${childRoomSummary.roomId}] autoJoin:${it.autoJoin}") +// val childRoomSummary = it.roomSummary ?: return@forEach + Timber.v("## Space: Processing child :[${it.childRoomId}] autoJoin:${it.autoJoin}") if (it.autoJoin) { // I should try to join as well - if (childRoomSummary.roomType == RoomType.SPACE) { + if (it.roomType == RoomType.SPACE) { // recursively join auto-joined child of this space? - when (val subspaceJoinResult = this.execute(JoinSpaceTask.Params(it.roomSummary.roomId, null, it.viaServers))) { + when (val subspaceJoinResult = this.execute(JoinSpaceTask.Params(it.childRoomId, null, it.viaServers))) { SpaceService.JoinSpaceResult.Success -> { // nop } is SpaceService.JoinSpaceResult.Fail -> { - errors[it.roomSummary.roomId] = subspaceJoinResult.error + errors[it.childRoomId] = subspaceJoinResult.error } is SpaceService.JoinSpaceResult.PartialSuccess -> { errors.putAll(subspaceJoinResult.failedRooms) @@ -105,15 +106,15 @@ internal class DefaultJoinSpaceTask @Inject constructor( } } else { try { - Timber.v("## Space: Joining room child ${childRoomSummary.roomId}") + Timber.v("## Space: Joining room child ${it.childRoomId}") joinRoomTask.execute(JoinRoomTask.Params( - roomIdOrAlias = childRoomSummary.roomId, + roomIdOrAlias = it.childRoomId, reason = "Auto-join parent space", viaServers = it.viaServers )) } catch (failure: Throwable) { - errors[it.roomSummary.roomId] = failure - Timber.e("## Space: Failed to join room child ${childRoomSummary.roomId}") + errors[it.childRoomId] = failure + Timber.e("## Space: Failed to join room child ${it.childRoomId}") } } } 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 deleted file mode 100644 index c15e81c287..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryDataSource.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt index 95fbb2f1b0..5a1de41b95 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -95,6 +95,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter) handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter) handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, aggregator, reporter) + + // post room sync validation + roomSummaryUpdater.validateSpaceRelationship(realm) } // PRIVATE METHODS ***************************************************************************** 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 index d95251c271..e8293d7e99 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/SelectedSpaceDataSource.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/SelectedSpaceDataSource.kt @@ -18,9 +18,9 @@ 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 org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject import javax.inject.Singleton @Singleton -class SelectedSpaceDataSource @Inject constructor() : BehaviorDataSource>(Option.empty()) +class SelectedSpaceDataSource @Inject constructor() : BehaviorDataSource>(Option.empty()) 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 6bc3f27fd4..faf85124d3 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 @@ -55,7 +55,7 @@ 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.session.room.model.RoomSummary import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import timber.log.Timber @@ -252,10 +252,10 @@ class HomeDetailFragment @Inject constructor( } } - private fun onSpaceChange(spaceSummary: SpaceSummary?) { + private fun onSpaceChange(spaceSummary: RoomSummary?) { spaceSummary?.let { // Use GlideApp with activity context to avoid the glideRequests to be paused - if (spaceSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) { + if (spaceSummary.roomId == ALL_COMMUNITIES_GROUP_ID) { // Special case views.groupToolbarAvatarImageView.background = ContextCompat.getDrawable(requireContext(), R.drawable.space_home_background) views.groupToolbarAvatarImageView.scaleType = ImageView.ScaleType.CENTER_INSIDE 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 dd316dcece..d2ca7e9115 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,12 +22,11 @@ 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 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/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 47bc60eb75..06541c1fc3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -60,19 +60,21 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_GUEST_ACCESS, - EventType.REDACTION, EventType.STATE_ROOM_ALIASES, - EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_MAC, EventType.CALL_CANDIDATES, + EventType.KEY_VERIFICATION_MAC, EventType.CALL_REPLACES, EventType.CALL_SELECT_ANSWER, EventType.CALL_NEGOTIATE, EventType.REACTION, - EventType.STATE_ROOM_POWER_LEVELS -> noticeItemFactory.create(params) + EventType.STATE_ROOM_POWER_LEVELS, + EventType.STATE_SPACE_CHILD, + EventType.STATE_SPACE_PARENT, + EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) EventType.STATE_ROOM_WIDGET_LEGACY, EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params) EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 878cec0a07..71504daeff 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -107,6 +107,8 @@ class NoticeEventFormatter @Inject constructor( EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_READY, + EventType.STATE_SPACE_CHILD, + EventType.STATE_SPACE_PARENT, EventType.REDACTION -> formatDebug(timelineEvent.root) else -> { Timber.v("Type $type not handled by this formatter") diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt index 31dc537395..a7b5543157 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt @@ -212,6 +212,6 @@ class MatrixToRoomSpaceFragment @Inject constructor( } } - fun secondaryButtonClicked() = withState(sharedViewModel) { state -> + private fun secondaryButtonClicked() = withState(sharedViewModel) { _ -> } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 27c29ae42b..548f0b986d 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -111,7 +111,7 @@ class DefaultNavigator @Inject constructor( } sessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(spaceId)?.spaceSummary()?.let { - Timber.d("## Nav: Switching to space $spaceId / ${it.roomSummary.name}") + Timber.d("## Nav: Switching to space $spaceId / ${it.name}") selectedSpaceDataSource.post(Option.just(it)) } ?: kotlin.run { Timber.d("## Nav: Failed to switch to space $spaceId") diff --git a/vector/src/main/java/im/vector/app/features/settings/SharedPreferenceLiveData.kt b/vector/src/main/java/im/vector/app/features/settings/SharedPreferenceLiveData.kt new file mode 100644 index 0000000000..08e83258a1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/SharedPreferenceLiveData.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings + +import android.content.SharedPreferences +import androidx.lifecycle.LiveData + +abstract class SharedPreferenceLiveData(protected val sharedPrefs: SharedPreferences, + protected val key: String, + private val defValue: T) : LiveData() { + + private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == this.key) { + value = getValueFromPreferences(key, defValue) + } + } + + abstract fun getValueFromPreferences(key: String, defValue: T): T + + override fun onActive() { + super.onActive() + value = getValueFromPreferences(key, defValue) + sharedPrefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener) + } + + override fun onInactive() { + sharedPrefs.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener) + super.onInactive() + } + + companion object { + fun booleanLiveData(sharedPrefs: SharedPreferences, key: String, defaultValue: Boolean): SharedPreferenceLiveData { + return object : SharedPreferenceLiveData(sharedPrefs, key, defaultValue) { + override fun getValueFromPreferences(key: String, defValue: Boolean): Boolean { + return this.sharedPrefs.getBoolean(key, defValue) + } + } + } + } +} 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 222c8da6b7..4d1c9b5728 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 @@ -20,6 +20,7 @@ import android.media.RingtoneManager import android.net.Uri import android.provider.MediaStore import androidx.core.content.edit +import androidx.lifecycle.LiveData import com.squareup.seismic.ShakeDetector import im.vector.app.BuildConfig import im.vector.app.R @@ -312,6 +313,14 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_LABS_USE_SPACES, false) } + fun labSpacesLive(): LiveData { + return SharedPreferenceLiveData.booleanLiveData( + defaultPrefs, + 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/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt index 6a4ea83d68..cc2ca4eb4d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt @@ -17,6 +17,9 @@ package im.vector.app.features.settings import im.vector.app.R +import im.vector.app.core.preference.VectorSwitchPreference +import im.vector.app.features.MainActivity +import im.vector.app.features.MainActivityArgs import javax.inject.Inject class VectorSettingsLabsFragment @Inject constructor( @@ -27,6 +30,11 @@ class VectorSettingsLabsFragment @Inject constructor( override val preferenceXmlRes = R.xml.vector_settings_labs override fun bindPref() { - // Nothing to do + findPreference(VectorPreferences.SETTINGS_LABS_USE_SPACES)!!.let { pref -> + pref.setOnPreferenceChangeListener { _, _ -> + MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = false)) + true + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt index d3fb225083..2a6e3dace9 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt @@ -61,7 +61,7 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment?, selected: SpaceSummary?) { + private fun buildGroupModels(summaries: List?, selected: RoomSummary?) { if (summaries.isNullOrEmpty()) { return } // show invites on top - summaries.filter { it.roomSummary.membership == Membership.INVITE } + summaries.filter { it.membership == Membership.INVITE } .let { invites -> if (invites.isNotEmpty()) { genericItemHeader { @@ -67,7 +67,7 @@ class SpaceSummaryController @Inject constructor( invites.forEach { spaceSummaryItem { avatarRenderer(avatarRenderer) - id(it.spaceId) + id(it.roomId) matrixItem(it.toMatrixItem()) selected(false) listener { callback?.onSpaceSelected(it) } @@ -87,19 +87,19 @@ class SpaceSummaryController @Inject constructor( } summaries - .filter { it.roomSummary.membership == Membership.JOIN } + .filter { it.membership == Membership.JOIN } .forEach { groupSummary -> - val isSelected = groupSummary.spaceId == selected?.spaceId - if (groupSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) { + val isSelected = groupSummary.roomId == selected?.roomId + if (groupSummary.roomId == ALL_COMMUNITIES_GROUP_ID) { homeSpaceSummaryItem { - id(groupSummary.spaceId) + id(groupSummary.roomId) selected(isSelected) listener { callback?.onSpaceSelected(groupSummary) } } } else { spaceSummaryItem { avatarRenderer(avatarRenderer) - id(groupSummary.spaceId) + id(groupSummary.roomId) matrixItem(groupSummary.toMatrixItem()) selected(isSelected) onLeave { callback?.onLeaveSpace(groupSummary) } @@ -119,8 +119,8 @@ class SpaceSummaryController @Inject constructor( } interface Callback { - fun onSpaceSelected(spaceSummary: SpaceSummary) - fun onLeaveSpace(spaceSummary: SpaceSummary) + fun onSpaceSelected(spaceSummary: RoomSummary) + fun onLeaveSpace(spaceSummary: RoomSummary) fun onAddSpaceSelected() } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt index 3f1971d9dc..ffd27446f4 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt @@ -18,6 +18,8 @@ package im.vector.app.features.spaces import android.widget.ImageView import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass @@ -37,6 +39,8 @@ abstract class SpaceSummaryItem : VectorEpoxyModel() { @EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var listener: (() -> Unit)? = null @EpoxyAttribute var onLeave: (() -> Unit)? = null + @EpoxyAttribute var toggleExpand: (() -> Unit)? = null + @EpoxyAttribute var expanded: Boolean? = null override fun bind(holder: Holder) { super.bind(holder) @@ -52,6 +56,26 @@ abstract class SpaceSummaryItem : VectorEpoxyModel() { } else { holder.leaveView.isVisible = false } + + when (expanded) { + null -> { + holder.collapseIndicator.isGone = true + } + else -> { + holder.collapseIndicator.isVisible = true + holder.collapseIndicator.setImageDrawable( + ContextCompat.getDrawable(holder.view.context, + if (expanded!!) R.drawable.ic_expand_less else R.drawable.ic_expand_more + ) + ) + holder.collapseIndicator.setOnClickListener( + DebouncedClickListener({ _ -> + toggleExpand?.invoke() + }) + ) + } + } + avatarRenderer.renderSpace(matrixItem, holder.avatarImageView) } @@ -65,5 +89,6 @@ abstract class SpaceSummaryItem : VectorEpoxyModel() { val groupNameView by bind(R.id.groupNameView) val rootView by bind(R.id.itemGroupLayout) val leaveView by bind(R.id.groupTmpLeave) + val collapseIndicator by bind(R.id.groupChildrenCollapse) } } 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 index 28bc358e36..de5ca6a137 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt @@ -42,15 +42,14 @@ 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.roomSummaryQueryParams -import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.internal.util.awaitCallback 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() - data class LeaveSpace(val spaceSummary: SpaceSummary) : SpaceListAction() + data class SelectSpace(val spaceSummary: RoomSummary) : SpaceListAction() + data class LeaveSpace(val spaceSummary: RoomSummary) : SpaceListAction() object AddSpace : SpaceListAction() } @@ -64,8 +63,8 @@ sealed class SpaceListViewEvents : VectorViewEvents { } data class SpaceListViewState( - val asyncSpaces: Async> = Uninitialized, - val selectedSpace: SpaceSummary? = null + val asyncSpaces: Async> = Uninitialized, + val selectedSpace: RoomSummary? = null ) : MvRxState class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: SpaceListViewState, @@ -96,7 +95,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp selectedSpaceDataSource .observe() .subscribe { - if (currentGroupId != it.orNull()?.spaceId) { + if (currentGroupId != it.orNull()?.roomId) { setState { copy( selectedSpace = it.orNull() @@ -111,8 +110,8 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp 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 + if (currentGroupId != spaceSummary.roomId) { + currentGroupId = spaceSummary.roomId _viewEvents.post(SpaceListViewEvents.OpenSpace) } val optionGroup = Option.just(spaceSummary) @@ -120,7 +119,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp } 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 }) + copy(selectedSpace = this.asyncSpaces()?.find { it.roomId == ALL_COMMUNITIES_GROUP_ID }) } } } @@ -138,17 +137,17 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp private fun handleSelectSpace(action: SpaceListAction.SelectSpace) = withState { state -> // get uptodate version of the space - val summary = session.spaceService().getSpaceSummaries(roomSummaryQueryParams { roomId = QueryStringValue.Equals(action.spaceSummary.spaceId) }) + val summary = session.spaceService().getSpaceSummaries(roomSummaryQueryParams { roomId = QueryStringValue.Equals(action.spaceSummary.roomId) }) .firstOrNull() - if (summary?.roomSummary?.membership == Membership.INVITE) { - _viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(summary.roomSummary.roomId)) + if (summary?.membership == Membership.INVITE) { + _viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(summary.roomId)) // viewModelScope.launch(Dispatchers.IO) { // tryOrNull { session.spaceService().peekSpace(action.spaceSummary.spaceId) }.let { // Timber.d("PEEK RESULT/ $it") // } // } } else { - if (state.selectedSpace?.spaceId != action.spaceSummary.spaceId) { + if (state.selectedSpace?.roomId != action.spaceSummary.roomId) { // state.selectedSpace?.let { // selectedSpaceDataSource.post(Option.just(state.selectedSpace)) // } @@ -160,8 +159,8 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp private fun handleLeaveSpace(action: SpaceListAction.LeaveSpace) { viewModelScope.launch { awaitCallback { - tryOrNull("Failed to leave space ${action.spaceSummary.spaceId}") { - session.spaceService().getSpace(action.spaceSummary.spaceId)?.asRoom()?.leave(null, it) + tryOrNull("Failed to leave space ${action.spaceSummary.roomId}") { + session.spaceService().getSpace(action.spaceSummary.roomId)?.asRoom()?.leave(null, it) } } } @@ -178,23 +177,19 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp excludeType = listOf(/**RoomType.MESSAGING,$*/ null) } - Observable.combineLatest, List>( + 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() + 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() ) }, session @@ -205,9 +200,9 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp } ) .execute { async -> - val currentSelectedGroupId = selectedSpace?.spaceId + val currentSelectedGroupId = selectedSpace?.roomId val newSelectedGroup = if (currentSelectedGroupId != null) { - async()?.find { it.spaceId == currentSelectedGroupId } + async()?.find { it.roomId == currentSelectedGroupId } } else { async()?.firstOrNull() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt index a5d1e16101..31f7162c2a 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt @@ -35,8 +35,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap @@ -45,7 +45,7 @@ data class SpaceDirectoryState( // The current filter val spaceId: String, val currentFilter: String = "", - val summary: Async = Uninitialized, + val summary: Async = Uninitialized, // True if more result are available server side val hasMore: Boolean = false, // Set of joined roomId / spaces, diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt index 987884d8c9..66a702f685 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt @@ -156,12 +156,12 @@ class SpacePreviewViewModel @AssistedInject constructor( childInfoList = Success( resolveResult.second.map { ChildInfo( - roomId = it.roomSummary?.roomId ?: "", - avatarUrl = it.roomSummary?.avatarUrl, - name = it.roomSummary?.name, - topic = it.roomSummary?.topic, - memberCount = it.roomSummary?.joinedMembersCount, - isSubSpace = it.roomSummary?.roomType == RoomType.SPACE, + roomId = it.childRoomId, + avatarUrl = it.avatarUrl, + name = it.name, + topic = it.topic, + memberCount = it.activeMemberCount, + isSubSpace = it.roomType == RoomType.SPACE, children = Uninitialized, viaServers = null ) diff --git a/vector/src/main/res/layout/item_group.xml b/vector/src/main/res/layout/item_group.xml index 6c2721ff33..9cd07f3215 100644 --- a/vector/src/main/res/layout/item_group.xml +++ b/vector/src/main/res/layout/item_group.xml @@ -5,7 +5,7 @@ android:id="@+id/itemGroupLayout" android:layout_width="match_parent" android:layout_height="65dp" - android:background="@drawable/bg_group_item" + android:background="@drawable/bg_space_item" android:clickable="true" android:focusable="true" android:foreground="?attr/selectableItemBackground"> diff --git a/vector/src/main/res/layout/item_space.xml b/vector/src/main/res/layout/item_space.xml index 25b685b999..92f0fcc1d5 100644 --- a/vector/src/main/res/layout/item_space.xml +++ b/vector/src/main/res/layout/item_space.xml @@ -35,11 +35,28 @@ android:textSize="15sp" android:textStyle="bold" app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator" - app:layout_constraintEnd_toStartOf="@+id/groupTmpLeave" + app:layout_constraintEnd_toStartOf="@+id/groupChildrenCollapse" app:layout_constraintStart_toEndOf="@+id/groupAvatarImageView" app:layout_constraintTop_toTopOf="parent" tools:text="@tools:sample/lorem/random" /> + + + 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. + Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features. + Warning: This will trigger a clear cache and initial sync Link copied to clipboard diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index ed1ea222a2..aa55a3c5ec 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -49,6 +49,7 @@ + android:title="@string/labs_experimental_spaces" + android:summary="@string/labs_experimental_spaces_desc"/> \ No newline at end of file From 48fef45ce353ecfbe9b554f0af3ff028f14ae4f6 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 16 Mar 2021 09:36:56 +0100 Subject: [PATCH 26/97] Code quality --- .../database/RealmSessionStoreMigration.kt | 10 ++------- .../room/summary/RoomSummaryDataSource.kt | 22 +++++++++---------- tools/check/forbidden_strings_in_code.txt | 2 +- 3 files changed, 13 insertions(+), 21 deletions(-) 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 9dd8cda8b9..48479fd746 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 @@ -205,20 +205,14 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { ?.addField(SpaceChildSummaryEntityFields.AUTO_JOIN, Boolean::class.java) ?.setNullable(SpaceChildSummaryEntityFields.AUTO_JOIN, true) ?.addRealmObjectField(SpaceChildSummaryEntityFields.CHILD_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) - -// realm.schema.create("SpaceSummaryEntity") -// ?.addField(SpaceSummaryEntityFields.SPACE_ID, String::class.java, FieldAttribute.PRIMARY_KEY) -// ?.setRequired(SpaceSummaryEntityFields.SPACE_ID, true) -// ?.addRealmObjectField(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) -// ?.addRealmListField(SpaceSummaryEntityFields.CHILDREN.`$`, spaceChildInfoSchema!!) + ?.addRealmListField(SpaceChildSummaryEntityFields.VIA_SERVERS.`$`, String::class.java) realm.schema.create("SpaceParentSummaryEntity") ?.addField(SpaceParentSummaryEntityFields.PARENT_ROOM_ID, String::class.java) ?.addField(SpaceParentSummaryEntityFields.CANONICAL, Boolean::class.java) ?.setNullable(SpaceParentSummaryEntityFields.CANONICAL, true) -// ?.addRealmListField(RoomParentRelationInfoEntityFields.VIA_SERVERS.`$`, String::class.java) -// ?.addRealmObjectField(RoomParentRelationInfoEntityFields.SPACE_SUMMARY_ENTITY.`$`, realm.schema.get("SpaceSummaryEntity")!!) ?.addRealmObjectField(SpaceParentSummaryEntityFields.PARENT_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) + ?.addRealmListField(SpaceParentSummaryEntityFields.VIA_SERVERS.`$`, String::class.java) realm.schema.get("RoomSummaryEntity") ?.addField(RoomSummaryEntityFields.ROOM_TYPE, String::class.java) 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 2468661ada..fd54ac63ee 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 @@ -274,14 +274,13 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat } fun getFlattenOrphanRooms(): List { - return getRoomSummaries(roomSummaryQueryParams { - memberships = Membership.activeMemberships() - excludeType = listOf(RoomType.SPACE) - roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS - }).filter { - // we need to check if orphan - isOrphan(it) - } + return getRoomSummaries( + roomSummaryQueryParams { + memberships = Membership.activeMemberships() + excludeType = listOf(RoomType.SPACE) + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + } + ).filter { isOrphan(it) } } fun getFlattenOrphanRoomsLive(): LiveData> { @@ -290,10 +289,9 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat memberships = Membership.activeMemberships() excludeType = listOf(RoomType.SPACE) roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS - })) { - it.filter { - isOrphan(it) - } + }) + ) { + it.filter { isOrphan(it) } } } diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 5a53ececec..a4ec75e19f 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===94 +enum class===97 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 From e364a36ee6acbbb65676d3a8b23555a36412d79e Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 16 Mar 2021 09:42:10 +0100 Subject: [PATCH 27/97] cleaning --- .../java/org/matrix/android/sdk/api/session/room/Room.kt | 3 ++- .../internal/database/model/SpaceParentSummaryEntity.kt | 4 ---- .../sdk/internal/session/space/DefaultSpaceService.kt | 7 ------- .../im/vector/app/features/permalink/PermalinkHandler.kt | 2 +- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 5f2bc716f6..1bbc090ef0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -92,6 +92,7 @@ interface Room : limit: Int, beforeLimit: Int, afterLimit: Int, -// fun getSpaceParents(): List includeProfile: Boolean): SearchResult + + fun asSpace(): Space? } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt index af32cd2b83..30517717f4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt @@ -39,10 +39,6 @@ internal open class SpaceParentSummaryEntity( var viaServers: RealmList = RealmList() -// var child: RoomSummaryEntity? = null, - -// var level: Int = 0 - ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index cdd2673fde..ab104901d5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -51,21 +51,14 @@ internal class DefaultSpaceService @Inject constructor( @SessionDatabase private val monarchy: Monarchy, @UserId private val userId: String, private val createSpaceTask: CreateSpaceTask, -// private val joinRoomTask: JoinRoomTask, private val joinSpaceTask: JoinSpaceTask, private val spaceGetter: SpaceGetter, -// private val markAllRoomsReadTask: MarkAllRoomsReadTask, -// private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, -// private val roomIdByAliasTask: GetRoomIdByAliasTask, -// private val deleteRoomAliasTask: DeleteRoomAliasTask, private val roomGetter: RoomGetter, private val roomSummaryDataSource: RoomSummaryDataSource, private val stateEventDataSource: StateEventDataSource, private val peekSpaceTask: PeekSpaceTask, private val resolveSpaceInfoTask: ResolveSpaceInfoTask, private val leaveRoomTask: LeaveRoomTask -// private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, -// private val taskExecutor: TaskExecutor ) : SpaceService { override suspend fun createSpace(params: CreateSpaceParams): String { diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt index 1fea25159f..aa977730db 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt @@ -148,7 +148,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti val roomSummary = session.getRoomSummary(roomId) val membership = roomSummary?.membership val eventId = permalinkData.eventId - val roomAlias = permalinkData.getRoomAliasOrNull() +// val roomAlias = permalinkData.getRoomAliasOrNull() val isSpace = roomSummary?.roomType == RoomType.SPACE return when { membership == Membership.BAN -> context.toast(R.string.error_opening_banned_room) From 01c56824b7456d1b43660cca6015f216df0a3074 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 16 Mar 2021 10:25:46 +0100 Subject: [PATCH 28/97] small a11Y fix --- vector/src/main/res/layout/item_space_top_summary.xml | 1 + vector/src/main/res/layout/view_space_type_button.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/vector/src/main/res/layout/item_space_top_summary.xml b/vector/src/main/res/layout/item_space_top_summary.xml index e4e2bbdd76..35ca5ebf34 100644 --- a/vector/src/main/res/layout/item_space_top_summary.xml +++ b/vector/src/main/res/layout/item_space_top_summary.xml @@ -11,6 +11,7 @@ android:layout_width="16dp" android:layout_height="16dp" android:src="@drawable/ic_room_profile_member_list" + android:importantForAccessibility="no" app:layout_constraintBottom_toBottomOf="@+id/spaceSummaryMemberCountText" app:layout_constraintStart_toEndOf="@+id/spaceSummaryMemberCountText" app:layout_constraintStart_toStartOf="parent" diff --git a/vector/src/main/res/layout/view_space_type_button.xml b/vector/src/main/res/layout/view_space_type_button.xml index ac20b74d4a..6ddb126469 100644 --- a/vector/src/main/res/layout/view_space_type_button.xml +++ b/vector/src/main/res/layout/view_space_type_button.xml @@ -61,6 +61,7 @@ android:id="@+id/rightChevron" android:layout_width="16dp" android:layout_height="16dp" + android:importantForAccessibility="no" android:src="@drawable/ic_material_chevron_right_black" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" From 0da9be327ac2fc0ba302c6665563600fe22cfc22 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 16 Mar 2021 11:00:39 +0100 Subject: [PATCH 29/97] Removed unneeded id.home menu handling --- .../vector/app/features/spaces/SpaceCreationActivity.kt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt index ed8b85f587..c5dd09ee84 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt @@ -19,7 +19,6 @@ package im.vector.app.features.spaces import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.MenuItem import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import com.airbnb.mvrx.Loading @@ -65,14 +64,6 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - onBackPressed() - return true - } - return super.onOptionsItemSelected(item) - } - override fun initUiAndData() { super.initUiAndData() viewModel.subscribe(this) { From 802853d205ae9d57e6dcb73bd8fa14a426ca2cbb Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 18 Mar 2021 09:42:21 +0100 Subject: [PATCH 30/97] Suggested Space support --- .../android/sdk/api/session/space/Space.kt | 7 +- .../session/space/model/SpaceChildContent.kt | 9 +- .../internal/session/space/DefaultSpace.kt | 8 +- .../sdk/internal/session/space/SpaceApi.kt | 2 +- .../session/space/SpaceSummaryParams.kt | 5 +- .../session/sync/ReadReceiptHandler.kt | 2 +- .../java/im/vector/app/AppStateHandler.kt | 11 ++ .../im/vector/app/core/di/VectorComponent.kt | 4 + ...CurrentSpaceSuggestedRoomListDataSource.kt | 25 ++++ .../app/features/home/HomeDetailFragment.kt | 3 +- .../features/home/room/list/RoomListAction.kt | 1 + .../home/room/list/RoomListFragment.kt | 5 + .../home/room/list/RoomListViewModel.kt | 38 +++++- .../home/room/list/RoomSummaryItemFactory.kt | 18 +++ .../home/room/list/SuggestedRoomItem.kt | 117 +++++++++++++++++ .../matrixto/MatrixToBottomSheetViewModel.kt | 32 ++++- .../features/spaces/SpacesListViewModel.kt | 5 +- .../spaces/create/CreateSpaceViewModelTask.kt | 16 ++- .../ui/SharedPreferencesUiStateRepository.kt | 14 +- .../app/features/ui/UiStateRepository.kt | 4 + .../main/res/layout/item_suggested_room.xml | 123 ++++++++++++++++++ vector/src/main/res/values/strings.xml | 2 + 22 files changed, 430 insertions(+), 21 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/CurrentSpaceSuggestedRoomListDataSource.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomItem.kt create mode 100644 vector/src/main/res/layout/item_suggested_room.xml 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 index cfbffc128c..ba76276fde 100644 --- 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 @@ -21,14 +21,17 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary interface Space { - fun asRoom() : Room + fun asRoom(): Room /** * A current snapshot of [RoomSummary] associated with the space */ fun spaceSummary(): RoomSummary? - suspend fun addChildren(roomId: String, viaServers: List, order: String?, autoJoin: Boolean = false) + suspend fun addChildren(roomId: String, viaServers: List, + order: String?, + autoJoin: Boolean = false, + suggested: Boolean? = false) suspend fun removeRoom(roomId: String) 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 index f7abf7e618..f84d781eb6 100644 --- 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 @@ -46,7 +46,14 @@ data class SpaceChildContent( * be automatically joined by members of that space. * (This is not a force-join, which are descoped for a future MSC; the user can subsequently part these room if they desire.) */ - @Json(name = "auto_join") val autoJoin: Boolean? = false + @Json(name = "auto_join") val autoJoin: Boolean? = false, + + /** + * If `suggested` is set to `true`, that indicates that the child should be advertised to + * members of the space by the client. This could be done by showing them eagerly + * in the room list. This is should be ignored if `auto_join` is set to `true`. + */ + @Json(name = "suggested") val suggested: Boolean? = false ) { /** * Orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7F (~), 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 index 13b9465d58..76496769ec 100644 --- 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 @@ -36,14 +36,18 @@ internal class DefaultSpace(private val room: Room, private val spaceSummaryData return spaceSummaryDataSource.getSpaceSummary(asRoom().roomId) } - override suspend fun addChildren(roomId: String, viaServers: List, order: String?, autoJoin: Boolean) { + override suspend fun addChildren(roomId: String, viaServers: List, + order: String?, + autoJoin: Boolean, + suggested: Boolean?) { asRoom().sendStateEvent( eventType = EventType.STATE_SPACE_CHILD, stateKey = roomId, body = SpaceChildContent( via = viaServers, autoJoin = autoJoin, - order = order + order = order, + suggested = suggested ).toContent() ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt index 5919a90b99..249ebd2fd2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt @@ -35,7 +35,7 @@ internal interface SpaceApi { * * MSC 2946 https://github.com/matrix-org/matrix-doc/blob/kegan/spaces-summary/proposals/2946-spaces-summary.md */ - @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/spaces") + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/spaces") fun getSpaces(@Path("roomId") spaceId: String, @Body params: SpaceSummaryParams ): Call diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt index a3c6b3cc84..af5aec0554 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt @@ -26,5 +26,8 @@ internal data class SpaceSummaryParams( /** The maximum number of rooms/subspaces to return, server can override this, default: 100 */ @Json(name = "limit") val limit: Int = 100, /** A token to use if this is a subsequent HTTP hit, default: "".*/ - @Json(name = "batch") val batch: String = "" + @Json(name = "batch") val batch: String = "", + /** whether we should only return children with the "suggested" flag set.*/ + @Json(name = "suggested_only") val suggestedOnly: Boolean = false + ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt index e5d9217db7..fc1a2c3870 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt @@ -143,7 +143,7 @@ internal class ReadReceiptHandler @Inject constructor( @Suppress("UNCHECKED_CAST") val content = dataFromFile .events - .firstOrNull { it.type == EventType.RECEIPT } + ?.firstOrNull { it.type == EventType.RECEIPT } ?.content as? ReadReceiptContent if (content == null) { diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index edec704f18..22816085fb 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -33,6 +33,17 @@ class AppStateHandler @Inject constructor() : LifecycleObserver { private val compositeDisposable = CompositeDisposable() + init { + // restore current space from ui state + sessionDataSource.currentValue?.orNull()?.let { session -> + uiStateRepository.getSelectedSpace(session.sessionId)?.let { selectedSpaceId -> + session.getRoomSummary(selectedSpaceId)?.let { + selectedSpaceDataSource.post(Option.just(it)) + } + } + } + } + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun entersForeground() { } 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 3a197d3f83..e75f2416eb 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 @@ -37,6 +37,8 @@ import im.vector.app.features.crypto.verification.IncomingVerificationRequestHan 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.CurrentSpaceSuggestedRoomListDataSource +import im.vector.app.features.home.HomeRoomListDataSource import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder @@ -118,6 +120,8 @@ interface VectorComponent { fun selectedSpaceStore(): SelectedSpaceDataSource + fun currentSpaceSuggestedRoomListDataSource(): CurrentSpaceSuggestedRoomListDataSource + fun roomDetailPendingActionStore(): RoomDetailPendingActionStore fun activeSessionObservableStore(): ActiveSessionDataSource diff --git a/vector/src/main/java/im/vector/app/features/home/CurrentSpaceSuggestedRoomListDataSource.kt b/vector/src/main/java/im/vector/app/features/home/CurrentSpaceSuggestedRoomListDataSource.kt new file mode 100644 index 0000000000..21fd37c8fc --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/CurrentSpaceSuggestedRoomListDataSource.kt @@ -0,0 +1,25 @@ +/* + * 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.home + +import im.vector.app.core.utils.BehaviorDataSource +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CurrentSpaceSuggestedRoomListDataSource @Inject constructor() : BehaviorDataSource>() 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 faf85124d3..1f600de9e1 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 @@ -58,7 +58,6 @@ 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.util.toMatrixItem import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo -import timber.log.Timber import javax.inject.Inject private const val INDEX_PEOPLE = 0 @@ -363,7 +362,7 @@ class HomeDetailFragment @Inject constructor( } override fun invalidate() = withState(viewModel) { - Timber.v(it.toString()) +// Timber.v(it.toString()) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_people).render(it.notificationCountPeople, it.notificationHighlightPeople) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt index 883efb2e60..37f7d148aa 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt @@ -29,4 +29,5 @@ sealed class RoomListAction : VectorViewModelAction { data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : RoomListAction() data class ToggleTag(val roomId: String, val tag: String) : RoomListAction() data class LeaveRoom(val roomId: String) : RoomListAction() + data class JoinSuggestedRoom(val roomId: String, val viaServers: List?) : RoomListAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index aaa5bbcde5..edc3de58e0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -52,6 +52,7 @@ import im.vector.app.features.notifications.NotificationDrawerManager import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import javax.inject.Inject @@ -421,6 +422,10 @@ class RoomListFragment @Inject constructor( roomListViewModel.handle(RoomListAction.AcceptInvitation(room)) } + override fun onJoinSuggestedRoom(room: SpaceChildInfo) { + roomListViewModel.handle(RoomListAction.JoinSuggestedRoom(room.childRoomId, room.viaServers)) + } + override fun onRejectRoomInvitation(room: RoomSummary) { notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId) roomListViewModel.handle(RoomListAction.RejectInvitation(room)) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 423a950591..45a6f2218f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -18,8 +18,11 @@ package im.vector.app.features.home.room.list import androidx.annotation.StringRes import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext import im.vector.app.R import im.vector.app.core.extensions.exhaustive @@ -166,6 +169,7 @@ class RoomListViewModel @Inject constructor( is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is RoomListAction.ToggleTag -> handleToggleTag(action) is RoomListAction.ToggleSection -> handleToggleSection(action.section) + is RoomListAction.JoinSuggestedRoom -> handleJoinSuggestedRoom(action) }.exhaustive } @@ -316,6 +320,38 @@ class RoomListViewModel @Inject constructor( } } + private fun handleJoinSuggestedRoom(action: RoomListAction.JoinSuggestedRoom) { + setState { + copy( + suggestedRoomJoiningState = this.suggestedRoomJoiningState.toMutableMap().apply { + this[action.roomId] = Loading() + }.toMap() + ) + } + viewModelScope.launch { + try { + awaitCallback { + session.joinRoom(action.roomId, null, action.viaServers ?: emptyList(), it) + } + setState { + copy( + suggestedRoomJoiningState = this.suggestedRoomJoiningState.toMutableMap().apply { + this[action.roomId] = Success(Unit) + }.toMap() + ) + } + } catch (failure: Throwable) { + setState { + copy( + suggestedRoomJoiningState = this.suggestedRoomJoiningState.toMutableMap().apply { + this[action.roomId] = Fail(failure) + }.toMap() + ) + } + } + } + } + private fun handleToggleTag(action: RoomListAction.ToggleTag) { session.getRoom(action.roomId)?.let { room -> viewModelScope.launch(Dispatchers.IO) { @@ -342,7 +378,7 @@ class RoomListViewModel @Inject constructor( private fun String.otherTag(): String? { return when (this) { - RoomTag.ROOM_TAG_FAVOURITE -> RoomTag.ROOM_TAG_LOW_PRIORITY + RoomTag.ROOM_TAG_FAVOURITE -> RoomTag.ROOM_TAG_LOW_PRIORITY RoomTag.ROOM_TAG_LOW_PRIORITY -> RoomTag.ROOM_TAG_FAVOURITE else -> null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index fa6c970d8a..0c6651dd08 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -16,6 +16,9 @@ package im.vector.app.features.home.room.list +import android.view.View +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Loading import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter @@ -28,6 +31,8 @@ import im.vector.app.features.home.room.typing.TypingHelper import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -50,6 +55,19 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor } } + fun createSuggestion(spaceChildInfo: SpaceChildInfo, + suggestedRoomJoiningStates: Map>, + onJoinClick: View.OnClickListener) : VectorEpoxyModel<*> { + return SuggestedRoomItem_() + .id("sug_${spaceChildInfo.childRoomId}") + .matrixItem(MatrixItem.RoomItem(spaceChildInfo.childRoomId, spaceChildInfo.name, spaceChildInfo.avatarUrl)) + .avatarRenderer(avatarRenderer) + .topic(spaceChildInfo.topic) + .loading(suggestedRoomJoiningStates[spaceChildInfo.childRoomId] is Loading) + .memberCount(spaceChildInfo.activeMemberCount ?: 0) + .buttonClickListener(onJoinClick) + } + private fun createInvitationItem(roomSummary: RoomSummary, changeMembershipState: ChangeMembershipState, listener: RoomListListener?): VectorEpoxyModel<*> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomItem.kt new file mode 100644 index 0000000000..bdd196f750 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomItem.kt @@ -0,0 +1,117 @@ +/* + * 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.home.room.list + +import android.view.HapticFeedbackConstants +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +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.features.home.AvatarRenderer +import im.vector.app.features.themes.ThemeUtils +import me.gujun.android.span.image +import me.gujun.android.span.span +import org.matrix.android.sdk.api.util.MatrixItem + +@EpoxyModelClass(layout = R.layout.item_suggested_room) +abstract class SuggestedRoomItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute lateinit var matrixItem: MatrixItem + + // Used only for diff calculation + @EpoxyAttribute var topic: String? = null + + @EpoxyAttribute var memberCount: Int = 0 + @EpoxyAttribute var loading: Boolean = false + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemLongClickListener: View.OnLongClickListener? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: View.OnClickListener? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var buttonClickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.rootView.setOnClickListener(itemClickListener) + holder.rootView.setOnLongClickListener { + it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + itemLongClickListener?.onLongClick(it) ?: false + } + holder.titleView.text = matrixItem.getBestName() + avatarRenderer.render(matrixItem, holder.avatarImageView) + + holder.descriptionText.text = span { + span { + apply { + val tintColor = ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_secondary) + ContextCompat.getDrawable(holder.view.context, R.drawable.ic_room_profile_member_list) + ?.apply { + ThemeUtils.tintDrawableWithColor(this, tintColor) + }?.let { + image(it) + } + } + +" $memberCount" + apply { + topic?.let { + +" - $topic" + } + } + } + } + + if (loading) { + holder.joinButtonLoading.isVisible = true + holder.joinButton.isInvisible = true + } else { + holder.joinButtonLoading.isVisible = false + holder.joinButton.isVisible = true + } + + holder.joinButton.setOnClickListener { + // local echo + holder.joinButtonLoading.isVisible = true + holder.joinButton.isInvisible = true + buttonClickListener?.onClick(it) + } + } + + override fun unbind(holder: Holder) { + holder.rootView.setOnClickListener(null) + holder.rootView.setOnLongClickListener(null) + avatarRenderer.clear(holder.avatarImageView) + super.unbind(holder) + } + + class Holder : VectorEpoxyHolder() { + val titleView by bind(R.id.roomNameView) + val joinButton by bind