From e53801957c30d286736aeb0ab18489adf5649b22 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 19 Feb 2021 13:36:39 +0000 Subject: [PATCH 001/111] Add subscribeToChanges/unsubscribeToChanges to VectorPreferences --- .../app/features/settings/VectorPreferences.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 d3ef36a80b..a0fe525d3e 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 @@ -239,6 +239,20 @@ class VectorPreferences @Inject constructor(private val context: Context) { } private val defaultPrefs = DefaultSharedPreferences.getInstance(context) + + /** + * Allow subscribing and unsubscribing to configuration changes. This is + * particularly useful when you need to be notified of a configuration change + * in a background service, e.g. for the P2P demos. + */ + + public fun subscribeToChanges(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + defaultPrefs.registerOnSharedPreferenceChangeListener(listener) + } + + public fun unsubscribeToChanges(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + defaultPrefs.unregisterOnSharedPreferenceChangeListener(listener) + } /** * Clear the preferences. From adc461d2f3c28bf95957caba357995561b6e30b8 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 19 Feb 2021 14:23:38 +0000 Subject: [PATCH 002/111] Update VectorPreferences.kt --- .../im/vector/app/features/settings/VectorPreferences.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 a0fe525d3e..fbd53f09c5 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 @@ -16,6 +16,7 @@ package im.vector.app.features.settings import android.content.Context +import android.content.SharedPreferences import android.media.RingtoneManager import android.net.Uri import android.provider.MediaStore @@ -246,11 +247,11 @@ class VectorPreferences @Inject constructor(private val context: Context) { * in a background service, e.g. for the P2P demos. */ - public fun subscribeToChanges(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + fun subscribeToChanges(listener: SharedPreferences.OnSharedPreferenceChangeListener) { defaultPrefs.registerOnSharedPreferenceChangeListener(listener) } - public fun unsubscribeToChanges(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + fun unsubscribeToChanges(listener: SharedPreferences.OnSharedPreferenceChangeListener) { defaultPrefs.unregisterOnSharedPreferenceChangeListener(listener) } From 2b403371a3370b073908581ebb09d9528c045722 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 19 Feb 2021 15:17:42 +0000 Subject: [PATCH 003/111] Update VectorPreferences.kt --- .../java/im/vector/app/features/settings/VectorPreferences.kt | 2 -- 1 file changed, 2 deletions(-) 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 fbd53f09c5..eba7403a05 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 @@ -246,11 +246,9 @@ class VectorPreferences @Inject constructor(private val context: Context) { * particularly useful when you need to be notified of a configuration change * in a background service, e.g. for the P2P demos. */ - fun subscribeToChanges(listener: SharedPreferences.OnSharedPreferenceChangeListener) { defaultPrefs.registerOnSharedPreferenceChangeListener(listener) } - fun unsubscribeToChanges(listener: SharedPreferences.OnSharedPreferenceChangeListener) { defaultPrefs.unregisterOnSharedPreferenceChangeListener(listener) } From 171793d19017275b3daf64d5152d8254903604b2 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 22 Jun 2021 17:35:39 +0200 Subject: [PATCH 004/111] room version cap support + room upgrade --- .../homeserver/HomeServerCapabilities.kt | 6 +- .../session/homeserver/RoomVersionModel.kt | 32 +++++ .../android/sdk/api/session/room/Room.kt | 4 +- .../room/version/RoomVersionService.kt | 32 +++++ .../android/sdk/api/session/space/Space.kt | 3 + .../database/RealmSessionStoreMigration.kt | 9 +- .../mapper/HomeServerCapabilitiesMapper.kt | 23 ++- .../model/HomeServerCapabilitiesEntity.kt | 3 +- .../homeserver/GetCapabilitiesResult.kt | 22 ++- .../GetHomeServerCapabilitiesTask.kt | 5 + .../sdk/internal/session/room/DefaultRoom.kt | 7 +- .../sdk/internal/session/room/RoomAPI.kt | 11 ++ .../sdk/internal/session/room/RoomFactory.kt | 5 +- .../sdk/internal/session/room/RoomModule.kt | 5 + .../internal/session/room/RoomUpgradeBody.kt | 26 ++++ .../session/room/RoomUpgradeResponse.kt | 26 ++++ .../room/summary/RoomSummaryUpdater.kt | 2 +- .../room/version/DefaultRoomVersionService.kt | 85 +++++++++++ .../room/version/RoomVersionUpgradeTask.kt | 66 +++++++++ .../internal/session/space/DefaultSpace.kt | 6 + .../java/im/vector/app/VectorApplication.kt | 1 - .../im/vector/app/core/di/ScreenComponent.kt | 4 + .../VectorBaseBottomSheetDialogFragment.kt | 6 +- .../core/ui/list/GenericProgressBarItem.kt | 53 +++++++ .../app/core/ui/views/NotificationAreaView.kt | 6 +- .../im/vector/app/features/command/Command.kt | 3 +- .../app/features/command/CommandParser.kt | 10 +- .../app/features/command/ParsedCommand.kt | 1 + .../detail/JoinReplacementRoomBottomSheet.kt | 87 +++++++++++ .../home/room/detail/RoomDetailAction.kt | 1 + .../home/room/detail/RoomDetailFragment.kt | 32 ++++- .../home/room/detail/RoomDetailViewEvents.kt | 2 + .../home/room/detail/RoomDetailViewModel.kt | 27 ++++ .../room/detail/upgrade/MigrateRoomAction.kt | 25 ++++ .../detail/upgrade/MigrateRoomBottomSheet.kt | 126 ++++++++++++++++ .../detail/upgrade/MigrateRoomController.kt | 135 ++++++++++++++++++ .../detail/upgrade/MigrateRoomViewModel.kt | 116 +++++++++++++++ .../detail/upgrade/MigrateRoomViewState.kt | 41 ++++++ .../upgrade/UpgradeRoomViewModelTask.kt | 99 +++++++++++++ .../HomeserverSettingsController.kt | 37 ++++- .../layout/bottom_sheet_tombstone_join.xml | 48 +++++++ .../main/res/layout/item_generic_progress.xml | 6 + .../res/layout/item_timeline_event_create.xml | 4 +- .../res/layout/view_notification_area.xml | 28 ++-- vector/src/main/res/values/strings.xml | 27 +++- 45 files changed, 1257 insertions(+), 46 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt create mode 100644 vector/src/main/java/im/vector/app/core/ui/list/GenericProgressBarItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt create mode 100644 vector/src/main/res/layout/bottom_sheet_tombstone_join.xml create mode 100644 vector/src/main/res/layout/item_generic_progress.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index da99ab8d54..fc3d09d91d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.session.homeserver +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities.Companion.MAX_UPLOAD_FILE_SIZE_UNKNOWN + data class HomeServerCapabilities( /** * True if it is possible to change the password of the account. @@ -32,7 +34,9 @@ data class HomeServerCapabilities( /** * Default identity server url, provided in Wellknown */ - val defaultIdentityServerUrl: String? = null + val defaultIdentityServerUrl: String? = null, + + val roomVersions: RoomVersionCapabilities? = null ) { companion object { const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt new file mode 100644 index 0000000000..5997df035e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt @@ -0,0 +1,32 @@ +/* + * 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.homeserver + +data class RoomVersionCapabilities( + val defaultRoomVersion: String, + val supportedVersion: List +) + +data class RoomVersionInfo( + val version: String, + val status: RoomVersionStatus +) + +enum class RoomVersionStatus { + STABLE, + UNSTABLE +} 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 cb04b05a74..ebe96b6382 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,7 @@ import org.matrix.android.sdk.api.session.room.tags.TagsService 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.room.version.RoomVersionService import org.matrix.android.sdk.api.session.search.SearchResult import org.matrix.android.sdk.api.session.space.Space import org.matrix.android.sdk.api.util.Optional @@ -57,7 +58,8 @@ interface Room : RelationService, RoomCryptoService, RoomPushRuleService, - RoomAccountDataService { + RoomAccountDataService, + RoomVersionService { /** * The roomId of this room diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt new file mode 100644 index 0000000000..a90e1343f2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt @@ -0,0 +1,32 @@ +/* + * 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.api.session.room.version + +interface RoomVersionService { + + fun getRoomVersion(): String + + /** + * Upgrade to the given room version + * @return the replacement room id + */ + suspend fun upgradeToVersion(version: String): String + + suspend fun getRecommendedVersion() : String + + fun userMayUpgradeRoom(userId: String): Boolean +} 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 db25762c2f..3bae6126e0 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 @@ -18,6 +18,7 @@ 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 +import org.matrix.android.sdk.api.session.space.model.SpaceChildContent interface Space { @@ -38,6 +39,8 @@ interface Space { autoJoin: Boolean = false, suggested: Boolean? = false) + fun getChildInfo(roomId: String): SpaceChildContent? + suspend fun removeChildren(roomId: String) @Throws 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 2b3c3b28ee..864aa13cc2 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 @@ -46,7 +46,7 @@ import javax.inject.Inject class RealmSessionStoreMigration @Inject constructor() : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 14L + const val SESSION_STORE_SCHEMA_VERSION = 15L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -66,6 +66,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { if (oldVersion <= 11) migrateTo12(realm) if (oldVersion <= 12) migrateTo13(realm) if (oldVersion <= 13) migrateTo14(realm) + if (oldVersion <= 14) migrateTo15(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -306,4 +307,10 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { roomAccountDataSchema.isEmbedded = true } + + private fun migrateTo15(realm: DynamicRealm) { + Timber.d("Step 14 -> 15") + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.ROOM_VERSION_JSON, String::class.java) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index b18c67294f..566df97105 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -16,8 +16,15 @@ package org.matrix.android.sdk.internal.database.mapper +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.homeserver.RoomVersionCapabilities +import org.matrix.android.sdk.api.session.homeserver.RoomVersionInfo +import org.matrix.android.sdk.api.session.homeserver.RoomVersionStatus import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.session.homeserver.RoomVersions +import org.matrix.android.sdk.internal.session.room.version.DefaultRoomVersionService /** * HomeServerCapabilitiesEntity -> HomeSeverCapabilities @@ -29,7 +36,21 @@ internal object HomeServerCapabilitiesMapper { canChangePassword = entity.canChangePassword, maxUploadFileSize = entity.maxUploadFileSize, lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported, - defaultIdentityServerUrl = entity.defaultIdentityServerUrl + defaultIdentityServerUrl = entity.defaultIdentityServerUrl, + roomVersions = entity.roomVersionJson?.let { + tryOrNull { + MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).fromJson(it)?.let { + RoomVersionCapabilities( + defaultRoomVersion = it.default ?: DefaultRoomVersionService.DEFAULT_ROOM_VERSION, + supportedVersion = it.available.entries.map { entry -> + RoomVersionInfo(entry.key, RoomVersionStatus.STABLE + .takeIf { entry.value == "stable" } + ?: RoomVersionStatus.UNSTABLE) + } + ) + } + } + } ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index 763dcf80a2..0af2fc4cc5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -24,7 +24,8 @@ internal open class HomeServerCapabilitiesEntity( var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN, var lastVersionIdentityServerSupported: Boolean = false, var defaultIdentityServerUrl: String? = null, - var lastUpdatedTimestamp: Long = 0L + var lastUpdatedTimestamp: Long = 0L, + var roomVersionJson: String? = null ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt index ab029a0fce..014b1b17da 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.homeserver import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.extensions.orTrue +import org.matrix.android.sdk.api.util.JsonDict /** * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-capabilities @@ -38,9 +39,14 @@ internal data class Capabilities( * Capability to indicate if the user can change their password. */ @Json(name = "m.change_password") - val changePassword: ChangePassword? = null + val changePassword: ChangePassword? = null, - // No need for m.room_versions for the moment + /** + * This capability describes the default and available room versions a server supports, and at what level of stability. + * Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms. + */ + @Json(name = "m.room_versions") + val roomVersions: RoomVersions? = null ) @JsonClass(generateAdapter = true) @@ -52,6 +58,18 @@ internal data class ChangePassword( val enabled: Boolean? ) +@JsonClass(generateAdapter = true) +internal data class RoomVersions( + /** + * Required. True if the user can change their password, false otherwise. + */ + @Json(name = "default") + val default: String?, + + @Json(name = "available") + val available: JsonDict +) + // The spec says: If not present, the client should assume that password changes are possible via the API internal fun GetCapabilitiesResult.canChangePassword(): Boolean { return capabilities?.changePassword?.enabled.orTrue() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index 740370123f..82eb03afe6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver @@ -104,6 +105,10 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( if (getCapabilitiesResult != null) { homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword() + + homeServerCapabilitiesEntity.roomVersionJson = getCapabilitiesResult.capabilities?.roomVersions?.let { + MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it) + } } if (getMediaConfigResult != null) { 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 0d9c106d41..1a95996024 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 @@ -37,6 +37,7 @@ import org.matrix.android.sdk.api.session.room.tags.TagsService 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.room.version.RoomVersionService import org.matrix.android.sdk.api.session.search.SearchResult import org.matrix.android.sdk.api.session.space.Space import org.matrix.android.sdk.api.util.Optional @@ -69,7 +70,8 @@ internal class DefaultRoom(override val roomId: String, private val roomAccountDataService: RoomAccountDataService, private val sendStateTask: SendStateTask, private val viaParameterFinder: ViaParameterFinder, - private val searchTask: SearchTask) : + private val searchTask: SearchTask, + private val roomVersionService: RoomVersionService) : Room, TimelineService by timelineService, SendService by sendService, @@ -85,7 +87,8 @@ internal class DefaultRoom(override val roomId: String, RelationService by relationService, MembershipService by roomMembersService, RoomPushRuleService by roomPushRuleService, - RoomAccountDataService by roomAccountDataService { + RoomAccountDataService by roomAccountDataService, + RoomVersionService by roomVersionService { override fun getRoomSummaryLive(): LiveData> { return roomSummaryDataSource.getRoomSummaryLive(roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 4f12604039..18ece60629 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -369,4 +369,15 @@ internal interface RoomAPI { @Path("roomId") roomId: String, @Path("type") type: String, @Body content: JsonDict) + + /** + * Upgrades the given room to a particular room version. + * Errors: + * 400, The request was invalid. One way this can happen is if the room version requested is not supported by the homeserver + * (M_UNSUPPORTED_ROOM_VERSION) + * 403: The user is not permitted to upgrade the room.(M_FORBIDDEN) + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/upgrade") + suspend fun upgradeRoom(@Path("roomId") roomId: String, + @Body body: RoomUpgradeBody): RoomUpgradeResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index 9ddb8f1177..6b5565fa50 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -37,6 +37,7 @@ import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService +import org.matrix.android.sdk.internal.session.room.version.DefaultRoomVersionService import org.matrix.android.sdk.internal.session.search.SearchTask import javax.inject.Inject @@ -61,6 +62,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: private val relationServiceFactory: DefaultRelationService.Factory, private val membershipServiceFactory: DefaultMembershipService.Factory, private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory, + private val roomVersionServiceFactory: DefaultRoomVersionService.Factory, private val roomAccountDataServiceFactory: DefaultRoomAccountDataService.Factory, private val sendStateTask: SendStateTask, private val viaParameterFinder: ViaParameterFinder, @@ -89,7 +91,8 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: roomAccountDataService = roomAccountDataServiceFactory.create(roomId), sendStateTask = sendStateTask, searchTask = searchTask, - viaParameterFinder = viaParameterFinder + viaParameterFinder = viaParameterFinder, + roomVersionService = roomVersionServiceFactory.create(roomId) ) } } 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 d88c195056..c04c899e18 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 @@ -92,6 +92,8 @@ 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.room.version.DefaultRoomVersionUpgradeTask +import org.matrix.android.sdk.internal.session.room.version.RoomVersionUpgradeTask import org.matrix.android.sdk.internal.session.space.DefaultSpaceService import retrofit2.Retrofit @@ -243,4 +245,7 @@ internal abstract class RoomModule { @Binds abstract fun bindGetEventTask(task: DefaultGetEventTask): GetEventTask + + @Binds + abstract fun bindRoomVersionUpgradeTask(task: DefaultRoomVersionUpgradeTask): RoomVersionUpgradeTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt new file mode 100644 index 0000000000..feefbe5f66 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.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 org.matrix.android.sdk.internal.session.room + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RoomUpgradeBody( + @Json(name = "new_version") + val newVersion: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.kt new file mode 100644 index 0000000000..16e95194a3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.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 org.matrix.android.sdk.internal.session.room + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RoomUpgradeResponse( + @Json(name = "replacement_room") + val replacementRoomId: String +) 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 7cbcfee713..7ad6d06b1e 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 @@ -354,7 +354,7 @@ internal class RoomSummaryUpdater @Inject constructor( // we keep real m.child/m.parent relations and add the one for common memberships dmRoom.flattenParentIds += "|${flattenRelated.joinToString("|")}|" } -// Timber.v("## SPACES: flatten of ${dmRoom.otherMemberIds.joinToString(",")} is ${dmRoom.flattenParentIds}") + Timber.v("## SPACES: flatten of ${dmRoom.otherMemberIds.joinToString(",")} is ${dmRoom.flattenParentIds}") } // Maybe a good place to count the number of notifications for spaces? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt new file mode 100644 index 0000000000..1b5d62926e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt @@ -0,0 +1,85 @@ +/* + * 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.room.version + +import com.zhuinden.monarchy.Monarchy +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.realm.Realm +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.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.version.RoomVersionService +import org.matrix.android.sdk.internal.database.mapper.HomeServerCapabilitiesMapper +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource + +internal class DefaultRoomVersionService @AssistedInject constructor( + @Assisted private val roomId: String, + @SessionDatabase private val monarchy: Monarchy, + private val stateEventDataSource: StateEventDataSource, + private val roomVersionUpgradeTask: RoomVersionUpgradeTask +) : RoomVersionService { + + @AssistedFactory + interface Factory { + fun create(roomId: String): DefaultRoomVersionService + } + + override fun getRoomVersion(): String { + return stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_CREATE, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + ?.roomVersion + // as per spec -> Defaults to "1" if the key does not exist. + ?: DEFAULT_ROOM_VERSION + } + + override suspend fun upgradeToVersion(version: String): String { + return roomVersionUpgradeTask.execute( + RoomVersionUpgradeTask.Params( + roomId, version + ) + ) + } + + override suspend fun getRecommendedVersion(): String { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + HomeServerCapabilitiesEntity.get(realm)?.let { + HomeServerCapabilitiesMapper.map(it) + }?.roomVersions?.defaultRoomVersion ?: DEFAULT_ROOM_VERSION + } + } + + override fun userMayUpgradeRoom(userId: String): Boolean { + val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + ?.content?.toModel() + ?.let { PowerLevelsHelper(it) } + + return powerLevelsHelper?.isUserAllowedToSend(userId, true, EventType.STATE_ROOM_TOMBSTONE) ?: false + } + + companion object { + const val DEFAULT_ROOM_VERSION = "1" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt new file mode 100644 index 0000000000..444795b5a9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt @@ -0,0 +1,66 @@ +/* + * 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.room.version + +import io.realm.RealmConfiguration +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult +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.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.RoomUpgradeBody +import org.matrix.android.sdk.internal.task.Task +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal interface RoomVersionUpgradeTask : Task { + data class Params( + val roomId: String, + val newVersion: String + ) +} + +internal class DefaultRoomVersionUpgradeTask @Inject constructor( + private val roomAPI: RoomAPI, + private val globalErrorReceiver: GlobalErrorReceiver, + @SessionDatabase + private val realmConfiguration: RealmConfiguration +) : RoomVersionUpgradeTask { + + override suspend fun execute(params: RoomVersionUpgradeTask.Params): String { + val replacementRoomId = executeRequest(globalErrorReceiver) { + roomAPI.upgradeRoom( + roomId = params.roomId, + body = RoomUpgradeBody(params.newVersion) + ) + }.replacementRoomId + + // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before) + tryOrNull { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomSummaryEntity::class.java) + .equalTo(RoomSummaryEntityFields.ROOM_ID, replacementRoomId) + .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + } + } + return replacementRoomId + } +} 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 70c52bf4ae..233eef45f8 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 @@ -86,6 +86,12 @@ internal class DefaultSpace( ) } + override fun getChildInfo(roomId: String): SpaceChildContent? { + return room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) + .firstOrNull() + ?.content.toModel() + } + override suspend fun setChildrenOrder(roomId: String, order: String?) { val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) .firstOrNull() diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index f3e2f8740e..37a9dc36b4 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -159,7 +159,6 @@ class VectorApplication : // Do not display the name change popup doNotShowDisclaimerDialog(this) } - if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!! activeSessionHolder.setActiveSession(lastAuthenticatedSession) 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 3c11bfcd13..7784a2bd1c 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 @@ -40,12 +40,14 @@ import im.vector.app.features.debug.DebugMenuActivity import im.vector.app.features.devtools.RoomDevToolActivity import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.HomeModule +import im.vector.app.features.home.room.detail.JoinReplacementRoomBottomSheet import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.app.features.home.room.detail.search.SearchActivity import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet +import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.home.room.filtered.FilteredRoomsActivity import im.vector.app.features.home.room.list.RoomListModule @@ -191,6 +193,8 @@ interface ScreenComponent { fun inject(bottomSheet: SpaceSettingsMenuBottomSheet) fun inject(bottomSheet: InviteRoomSpaceChooserBottomSheet) fun inject(bottomSheet: SpaceInviteBottomSheet) + fun inject(bottomSheet: JoinReplacementRoomBottomSheet) + fun inject(bottomSheet: MigrateRoomBottomSheet) /* ========================================================================================== * Others diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt index d6d4d07500..b9b5bc8ca5 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -46,7 +46,7 @@ import java.util.concurrent.TimeUnit /** * Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment) */ -abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment(), MvRxView { +abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment(), MvRxView { private val mvrxViewIdProperty = MvRxViewId() final override val mvrxViewId: String by mvrxViewIdProperty @@ -168,6 +168,10 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShee @CallSuper override fun invalidate() { + forceExpandState() + } + + protected fun forceExpandState() { if (showExpanded) { // Force the bottom sheet to be expanded bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericProgressBarItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericProgressBarItem.kt new file mode 100644 index 0000000000..48a267ec12 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericProgressBarItem.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.core.ui.list + +import android.widget.ProgressBar +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 + +/** + * A generic list item. + * Displays an item with a title, and optional description. + * Can display an accessory on the right, that can be an image or an indeterminate progress. + * If provided with an action, will display a button at the bottom of the list item. + */ +@EpoxyModelClass(layout = R.layout.item_generic_progress) +abstract class GenericProgressBarItem : VectorEpoxyModel() { + + @EpoxyAttribute + var progress: Int = 0 + + @EpoxyAttribute + var total: Int = 100 + + @EpoxyAttribute + var indeterminate: Boolean = false + + override fun bind(holder: Holder) { + super.bind(holder) + holder.progressbar.progress = progress + holder.progressbar.max = total + holder.progressbar.isIndeterminate = indeterminate + } + + class Holder : VectorEpoxyHolder() { + val progressbar by bind(R.id.genericProgressBar) + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt b/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt index ad2a4b8e0c..9e9fe9b8a0 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt @@ -21,7 +21,7 @@ import android.graphics.Color import android.text.method.LinkMovementMethod import android.util.AttributeSet import android.view.View -import android.widget.RelativeLayout +import android.widget.LinearLayout import androidx.core.content.ContextCompat import androidx.core.text.italic import im.vector.app.R @@ -44,7 +44,7 @@ class NotificationAreaView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : RelativeLayout(context, attrs, defStyleAttr) { +) : LinearLayout(context, attrs, defStyleAttr) { var delegate: Delegate? = null private var state: State = State.Initial @@ -127,7 +127,7 @@ class NotificationAreaView @JvmOverloads constructor( private fun renderTombstone(state: State.Tombstone) { visibility = View.VISIBLE - views.roomNotificationIcon.setImageResource(R.drawable.error) + views.roomNotificationIcon.setImageResource(R.drawable.ic_warning_badge) val message = span { +resources.getString(R.string.room_tombstone_versioned_description) +"\n" 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 61d39857cc..3719618d31 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 @@ -50,7 +50,8 @@ enum class Command(val command: String, val parameters: String, @StringRes val d 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); + LEAVE_ROOM("/leave", "", R.string.command_description_leave_room, true), + UPGRADE_ROOM("/upgraderoom", "newVersion", R.string.command_description_upgrade_room, true); 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 3de00f4d0c..224956049c 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 @@ -312,24 +312,28 @@ object CommandParser { ) } } - Command.ADD_TO_SPACE.command -> { + Command.ADD_TO_SPACE.command -> { val rawCommand = textMessage.substring(Command.ADD_TO_SPACE.command.length).trim() ParsedCommand.AddToSpace( rawCommand ) } - Command.JOIN_SPACE.command -> { + Command.JOIN_SPACE.command -> { val spaceIdOrAlias = textMessage.substring(Command.JOIN_SPACE.command.length).trim() ParsedCommand.JoinSpace( spaceIdOrAlias ) } - Command.LEAVE_ROOM.command -> { + Command.LEAVE_ROOM.command -> { val spaceIdOrAlias = textMessage.substring(Command.LEAVE_ROOM.command.length).trim() ParsedCommand.LeaveRoom( spaceIdOrAlias ) } + Command.UPGRADE_ROOM.command -> { + val newVersion = textMessage.substring(Command.UPGRADE_ROOM.command.length).trim() + ParsedCommand.UpgradeRoom(newVersion) + } 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 d67caac60a..123f1d3a36 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 @@ -61,4 +61,5 @@ sealed class ParsedCommand { class AddToSpace(val spaceId: String) : ParsedCommand() class JoinSpace(val spaceIdOrAlias: String) : ParsedCommand() class LeaveRoom(val roomId: String) : ParsedCommand() + class UpgradeRoom(val newVersion: String) : ParsedCommand() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt new file mode 100644 index 0000000000..972162645d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt @@ -0,0 +1,87 @@ +/* + * 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.home.room.detail + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +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.di.ScreenComponent +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.platform.ButtonStateView +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetTombstoneJoinBinding +import javax.inject.Inject + +class JoinReplacementRoomBottomSheet : + VectorBaseBottomSheetDialogFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + BottomSheetTombstoneJoinBinding.inflate(inflater, container, false) + + @Inject + lateinit var errorFormatter: ErrorFormatter + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + private val viewModel: RoomDetailViewModel by parentFragmentViewModel() + + override val showExpanded: Boolean + get() = true + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.roomUpgradeButton.retryClicked = object : ClickListener { + override fun invoke(view: View) { + withState(viewModel) { it.tombstoneEvent }?.let { + viewModel.handle(RoomDetailAction.HandleTombstoneEvent(it)) + } + } + } + + viewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling) { joinState -> + when (joinState) { + // it should never be Uninitialized + Uninitialized -> views.roomUpgradeButton.render(ButtonStateView.State.Loaded) + is Loading -> { + views.roomUpgradeButton.render(ButtonStateView.State.Loading) + views.descriptionText.setText(R.string.it_may_take_some_time) + } + is Success -> { + views.roomUpgradeButton.render(ButtonStateView.State.Loaded) + dismiss() + } + is Fail -> { + // display the error message + views.descriptionText.text = errorFormatter.toHumanReadable(joinState.error) + views.roomUpgradeButton.render(ButtonStateView.State.Error) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index c0e73823e4..0e090256d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -108,4 +108,5 @@ sealed class RoomDetailAction : VectorViewModelAction { // Failed messages object RemoveAllFailedMessages : RoomDetailAction() + data class RoomUpgradeSuccess(val replacementRoom: String): RoomDetailAction() } 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 8307e93576..156d3f713f 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 @@ -51,6 +51,7 @@ import androidx.core.view.ViewCompat import androidx.core.view.forEach import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.fragment.app.setFragmentResultListener import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -147,6 +148,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever +import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan @@ -306,6 +308,15 @@ class RoomDetailFragment @Inject constructor( private lateinit var emojiPopup: EmojiPopup + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle -> + bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId -> + roomDetailViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId)) + } + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) @@ -405,6 +416,8 @@ class RoomDetailFragment @Inject constructor( is RoomDetailViewEvents.StartChatEffect -> handleChatEffect(it.type) RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) + RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() + is RoomDetailViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) }.exhaustive } @@ -423,6 +436,19 @@ class RoomDetailFragment @Inject constructor( startActivity(intent) } + private fun handleRoomReplacement() { + // this will join a new room, it can take time and might fail + // so we need to report progress and retry + val tag = JoinReplacementRoomBottomSheet::javaClass.name + JoinReplacementRoomBottomSheet().show(childFragmentManager, tag) + } + + private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: RoomDetailViewEvents.ShowRoomUpgradeDialog) { + val tag = MigrateRoomBottomSheet::javaClass.name + MigrateRoomBottomSheet.newInstance(roomDetailArgs.roomId, roomDetailViewEvents.newVersion) + .show(parentFragmentManager, tag) + } + private fun handleChatEffect(chatEffect: ChatEffect) { when (chatEffect) { ChatEffect.CONFETTI -> { @@ -1306,16 +1332,14 @@ class RoomDetailFragment @Inject constructor( private fun renderTombstoneEventHandling(async: Async) { when (async) { is Loading -> { - // TODO Better handling progress - vectorBaseActivity.showWaitingView(getString(R.string.joining_room)) + // shown in bottom sheet } is Success -> { navigator.openRoom(vectorBaseActivity, async()) vectorBaseActivity.finish() } is Fail -> { - vectorBaseActivity.hideWaitingView() - vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error)) + // shown in bottom sheet } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 4d1e62da7e..458549bbac 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -94,4 +94,6 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() object StopChatEffects : RoomDetailViewEvents() + object RoomReplacementStarted : RoomDetailViewEvents() + data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean): RoomDetailViewEvents() } 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 751114c2d9..08e1d215c2 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 @@ -321,6 +321,11 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() RoomDetailAction.ResendAll -> handleResendAll() + is RoomDetailAction.RoomUpgradeSuccess -> { + setState { + copy(tombstoneEventHandling = Success(action.replacementRoom)) + } + } }.exhaustive } @@ -585,6 +590,11 @@ class RoomDetailViewModel @AssistedInject constructor( val viaServers = MatrixPatterns.extractServerNameFromId(action.event.senderId) ?.let { listOf(it) } .orEmpty() + // need to provide feedback as joining could take some time + _viewEvents.post(RoomDetailViewEvents.RoomReplacementStarted) + setState { + copy(tombstoneEventHandling = Loading()) + } viewModelScope.launch { val result = runCatchingToAsync { session.joinRoom(roomId, viaServers = viaServers) @@ -817,6 +827,23 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) popDraft() } + is ParsedCommand.UpgradeRoom -> { + _viewEvents.post( + RoomDetailViewEvents.ShowRoomUpgradeDialog( + slashCommandResult.newVersion, + room.roomSummary()?.isPublic ?: false + ) + ) +// session.coroutineScope.launch { +// try { +// room.upgradeToVersion(slashCommandResult.newVersion) +// } 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/home/room/detail/upgrade/MigrateRoomAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomAction.kt new file mode 100644 index 0000000000..cb65be7e28 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomAction.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.home.room.detail.upgrade + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class MigrateRoomAction : VectorViewModelAction { + data class SetAutoInvite(val autoInvite: Boolean) : MigrateRoomAction() + data class SetUpdateKnownParentSpace(val update: Boolean) : MigrateRoomAction() + object UpgradeRoom : MigrateRoomAction() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt new file mode 100644 index 0000000000..12a0fb222b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt @@ -0,0 +1,126 @@ +/* + * 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.home.room.detail.upgrade + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.setFragmentResult +import com.airbnb.epoxy.OnModelBuildFinishedListener +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetGenericListBinding +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +class MigrateRoomBottomSheet : + VectorBaseBottomSheetDialogFragment(), + MigrateRoomViewModel.Factory, MigrateRoomController.InteractionListener { + + @Parcelize + data class Args( + val roomId: String, + val newVersion: String + ) : Parcelable + + @Inject + lateinit var viewModelFactory: MigrateRoomViewModel.Factory + + override val showExpanded = true + + @Inject + lateinit var epoxyController: MigrateRoomController + + val viewModel: MigrateRoomViewModel by fragmentViewModel() + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + BottomSheetGenericListBinding.inflate(inflater, container, false) + + override fun invalidate() = withState(viewModel) { state -> + epoxyController.setData(state) + super.invalidate() + + when (val result = state.upgradingStatus) { + is Success -> { + val result = result.invoke() + if (result is UpgradeRoomViewModelTask.Result.Success) { + setFragmentResult(REQUEST_KEY, Bundle().apply { + putString(BUNDLE_KEY_REPLACEMENT_ROOM, result.replacementRoomId) + }) + dismiss() + } + } + } + } + + val postBuild = OnModelBuildFinishedListener { + view?.post { forceExpandState() } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + epoxyController.callback = this + views.bottomSheetRecyclerView.configureWith(epoxyController) + epoxyController.addModelBuildListener(postBuild) + } + + override fun onDestroyView() { + views.bottomSheetRecyclerView.cleanup() + epoxyController.removeModelBuildListener(postBuild) + super.onDestroyView() + } + + override fun create(initialState: MigrateRoomViewState): MigrateRoomViewModel { + return viewModelFactory.create(initialState) + } + + companion object { + + const val REQUEST_KEY = "MigrateRoomBottomSheetRequest" + const val BUNDLE_KEY_REPLACEMENT_ROOM = "BUNDLE_KEY_REPLACEMENT_ROOM" + + fun newInstance(roomId: String, newVersion: String) + : MigrateRoomBottomSheet { + return MigrateRoomBottomSheet().apply { + setArguments(Args(roomId, newVersion)) + } + } + } + + override fun onAutoInvite(autoInvite: Boolean) { + viewModel.handle(MigrateRoomAction.SetAutoInvite(autoInvite)) + } + + override fun onAutoUpdateParent(update: Boolean) { + viewModel.handle(MigrateRoomAction.SetUpdateKnownParentSpace(update)) + } + + override fun onConfirmUpgrade() { + viewModel.handle(MigrateRoomAction.UpgradeRoom) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt new file mode 100644 index 0000000000..8d037860d2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.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.home.room.detail.upgrade + +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.errorWithRetryItem +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.bottomsheet.bottomSheetTitleItem +import im.vector.app.core.ui.list.ItemStyle +import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.core.ui.list.genericProgressBarItem +import im.vector.app.features.form.formSubmitButtonItem +import im.vector.app.features.form.formSwitchItem +import javax.inject.Inject + +class MigrateRoomController @Inject constructor( + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter +) : TypedEpoxyController() { + + interface InteractionListener { + fun onAutoInvite(autoInvite: Boolean) + fun onAutoUpdateParent(update: Boolean) + fun onConfirmUpgrade() + } + + var callback: InteractionListener? = null + + override fun buildModels(data: MigrateRoomViewState?) { + data ?: return + + val host = this@MigrateRoomController + + bottomSheetTitleItem { + id("title") + title( + host.stringProvider.getString(if (data.isPublic) R.string.upgrade_public_room else R.string.upgrade_private_room) + ) + } + + genericFooterItem { + id("warning_text") + centered(false) + style(ItemStyle.NORMAL_TEXT) + text(host.stringProvider.getString(R.string.upgrade_room_warning)) + } + + genericFooterItem { + id("from_to_room") + centered(false) + style(ItemStyle.NORMAL_TEXT) + text(host.stringProvider.getString(R.string.upgrade_public_room_from_to, data.currentVersion, data.newVersion)) + } + + if (!data.isPublic && data.otherMemberCount > 0) { + formSwitchItem { + id("auto_invite") + switchChecked(data.shouldIssueInvites) + title(host.stringProvider.getString(R.string.upgrade_room_auto_invite)) + listener { switch -> host.callback?.onAutoInvite(switch) } + } + } + + if (data.knownParents.isNotEmpty()) { + formSwitchItem { + id("update_parent") + switchChecked(data.shouldUpdateKnownParents) + title(host.stringProvider.getString(R.string.upgrade_room_update_parent)) + listener { switch -> host.callback?.onAutoUpdateParent(switch) } + } + } + when (data.upgradingStatus) { + is Loading -> { + genericProgressBarItem { + id("upgrade_progress") + indeterminate(data.upgradingProgressIndeterminate) + progress(data.upgradingProgress) + total(data.upgradingProgressTotal) + } + } + is Success -> { + when (val result = data.upgradingStatus.invoke()) { + is UpgradeRoomViewModelTask.Result.Failure -> { + val errorText = when (result) { + is UpgradeRoomViewModelTask.Result.UnknownRoom -> { + // should not happen + host.stringProvider.getString(R.string.unknown_error) + } + is UpgradeRoomViewModelTask.Result.NotAllowed -> { + host.stringProvider.getString(R.string.upgrade_room_no_power_to_manage) + } + is UpgradeRoomViewModelTask.Result.ErrorFailure -> { + host.errorFormatter.toHumanReadable(result.throwable) + } + else -> null + } + errorWithRetryItem { + id("error") + text(errorText) + listener { host.callback?.onConfirmUpgrade() } + } + } + is UpgradeRoomViewModelTask.Result.Success -> { + // nop, dismisses + } + } + } + else -> { + formSubmitButtonItem { + id("migrate") + buttonTitleId(R.string.upgrade) + buttonClickListener { host.callback?.onConfirmUpgrade() } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt new file mode 100644 index 0000000000..be9dc1bc52 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.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 im.vector.app.features.home.room.detail.upgrade + +import com.airbnb.mvrx.ActivityViewModelContext +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 dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session + +class MigrateRoomViewModel @AssistedInject constructor( + @Assisted initialState: MigrateRoomViewState, + private val session: Session, + private val upgradeRoomViewModelTask: UpgradeRoomViewModelTask) + : VectorViewModel(initialState) { + + init { + val room = session.getRoom(initialState.roomId) + val summary = session.getRoomSummary(initialState.roomId) + setState { + copy( + currentVersion = room?.getRoomVersion(), + isPublic = summary?.isPublic ?: false, + otherMemberCount = summary?.otherMemberIds?.count() ?: 0, + knownParents = summary?.flattenParentIds ?: emptyList() + ) + } + } + + @AssistedFactory + interface Factory { + fun create(initialState: MigrateRoomViewState): MigrateRoomViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: MigrateRoomViewState): MigrateRoomViewModel? { + 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: MigrateRoomAction) { + when (action) { + is MigrateRoomAction.SetAutoInvite -> { + setState { + copy(shouldIssueInvites = action.autoInvite) + } + } + is MigrateRoomAction.SetUpdateKnownParentSpace -> { + setState { + copy(shouldUpdateKnownParents = action.update) + } + } + MigrateRoomAction.UpgradeRoom -> { + handleUpgradeRoom(action) + } + } + } + + val upgradingProgress: ((indeterminate: Boolean, progress: Int, total: Int) -> Unit) = { indeterminate, progress, total -> + setState { + copy( + upgradingProgress = progress, + upgradingProgressTotal = total, + upgradingProgressIndeterminate = indeterminate + ) + } + } + + private fun handleUpgradeRoom(action: MigrateRoomAction) = withState { state -> + val summary = session.getRoomSummary(state.roomId) + setState { + copy(upgradingStatus = Loading()) + } + session.coroutineScope.launch { + val result = upgradeRoomViewModelTask.execute(UpgradeRoomViewModelTask.Params( + roomId = state.roomId, + newVersion = state.newVersion, + userIdsToAutoInvite = summary?.otherMemberIds?.takeIf { state.shouldIssueInvites } ?: emptyList(), + parentSpaceToUpdate = summary?.flattenParentIds?.takeIf { state.shouldUpdateKnownParents } ?: emptyList(), + progressReporter = upgradingProgress + )) + + setState { + copy(upgradingStatus = Success(result)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt new file mode 100644 index 0000000000..78c280fb10 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt @@ -0,0 +1,41 @@ +/* + * 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.home.room.detail.upgrade + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized + +data class MigrateRoomViewState( + val roomId: String, + val newVersion: String, + val currentVersion: String? = null, + val isPublic: Boolean = false, + val shouldIssueInvites: Boolean = false, + val shouldUpdateKnownParents: Boolean = false, + val otherMemberCount: Int = 0, + val knownParents: List = emptyList(), + val upgradingStatus: Async = Uninitialized, + val upgradingProgress: Int = 0, + val upgradingProgressTotal: Int = 0, + val upgradingProgressIndeterminate: Boolean = true +) : MvRxState { + constructor(args: MigrateRoomBottomSheet.Args) : this( + roomId = args.roomId, + newVersion = args.newVersion + ) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt new file mode 100644 index 0000000000..32c8e6ee92 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.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.home.room.detail.upgrade + +import im.vector.app.core.platform.ViewModelTask +import im.vector.app.core.resources.StringProvider +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.Session +import timber.log.Timber +import javax.inject.Inject + +class UpgradeRoomViewModelTask @Inject constructor( + val session: Session, + val stringProvider: StringProvider +) : ViewModelTask { + + sealed class Result { + data class Success(val replacementRoomId: String) : Result() + abstract class Failure(val throwable: Throwable?) : Result() + object UnknownRoom : Failure(null) + object NotAllowed : Failure(null) + class ErrorFailure(throwable: Throwable) : Failure(throwable) + } + + data class Params( + val roomId: String, + val newVersion: String, + val userIdsToAutoInvite: List = emptyList(), + val parentSpaceToUpdate: List = emptyList(), + val progressReporter: ((indeterminate: Boolean, progress: Int, total: Int) -> Unit)? = null + ) + + override suspend fun execute(params: Params): Result { + params.progressReporter?.invoke(true, 0, 0) + + val room = session.getRoom(params.roomId) + ?: return Result.UnknownRoom + if (!room.userMayUpgradeRoom(session.myUserId)) { + return Result.NotAllowed + } + + val updatedRoomId = try { + room.upgradeToVersion(params.newVersion) + } catch (failure: Throwable) { + return Result.ErrorFailure(failure) + } + + val totalStep = params.userIdsToAutoInvite.size + params.parentSpaceToUpdate.size + var currentStep = 0 + params.userIdsToAutoInvite.forEach { + params.progressReporter?.invoke(false, currentStep, totalStep) + tryOrNull { + session.getRoom(updatedRoomId)?.invite(it) + } + currentStep++ + } + + params.parentSpaceToUpdate.forEach { parentId -> + params.progressReporter?.invoke(false, currentStep, totalStep) + // we try and silently fail + try { + session.getRoom(parentId)?.asSpace()?.let { parentSpace -> + val currentInfo = parentSpace.getChildInfo(params.roomId) + if (currentInfo != null) { + parentSpace.addChildren( + roomId = updatedRoomId, + viaServers = currentInfo.via, + order = currentInfo.order, + autoJoin = currentInfo.autoJoin ?: false, + suggested = currentInfo.suggested + ) + + parentSpace.removeChildren(params.roomId) + } + } + } catch (failure: Throwable) { + Timber.d("## Migrate: Failed to update space parent. cause: ${failure.localizedMessage}") + } finally { + currentStep++ + } + } + + return Result.Success(updatedRoomId) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt index 3217756a82..37f95b184e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt @@ -26,16 +26,20 @@ import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericWithValueItem import im.vector.app.features.discovery.settingsCenteredImageItem import im.vector.app.features.discovery.settingsInfoItem import im.vector.app.features.discovery.settingsSectionTitleItem +import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.federation.FederationVersion import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.homeserver.RoomVersionStatus import javax.inject.Inject class HomeserverSettingsController @Inject constructor( private val stringProvider: StringProvider, - private val errorFormatter: ErrorFormatter + private val errorFormatter: ErrorFormatter, + private val vectorPreferences: VectorPreferences ) : TypedEpoxyController() { var callback: Callback? = null @@ -118,5 +122,36 @@ class HomeserverSettingsController @Inject constructor( helperText(host.stringProvider.getString(R.string.settings_server_upload_size_content, "${limit / 1048576L} MB")) } } + + if (vectorPreferences.developerMode()) { + val roomCapabilities = data.homeServerCapabilities.roomVersions + if (roomCapabilities != null) { + settingsSectionTitleItem { + id("room_versions") + titleResId(R.string.settings_server_room_versions) + } + + genericWithValueItem { + id("room_version_default") + title(host.stringProvider.getString(R.string.settings_server_default_room_version)) + value(roomCapabilities.defaultRoomVersion) + } + + roomCapabilities.supportedVersion.forEach { + genericWithValueItem { + id("room_version_${it.version}") + title(it.version) + value( + host.stringProvider.getString( + when (it.status) { + RoomVersionStatus.STABLE -> R.string.settings_server_room_version_stable + RoomVersionStatus.UNSTABLE -> R.string.settings_server_room_version_unstable + } + ) + ) + } + } + } + } } } diff --git a/vector/src/main/res/layout/bottom_sheet_tombstone_join.xml b/vector/src/main/res/layout/bottom_sheet_tombstone_join.xml new file mode 100644 index 0000000000..3e7fd495a9 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_tombstone_join.xml @@ -0,0 +1,48 @@ + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_generic_progress.xml b/vector/src/main/res/layout/item_generic_progress.xml new file mode 100644 index 0000000000..29ad67efa0 --- /dev/null +++ b/vector/src/main/res/layout/item_generic_progress.xml @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_create.xml b/vector/src/main/res/layout/item_timeline_event_create.xml index ea881ccdd0..51da203d10 100644 --- a/vector/src/main/res/layout/item_timeline_event_create.xml +++ b/vector/src/main/res/layout/item_timeline_event_create.xml @@ -10,7 +10,7 @@ style="@style/Widget.Vector.TextView.Body" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" + android:layout_marginStart="0dp" android:layout_marginTop="16dp" android:layout_marginBottom="16dp" android:background="?vctr_keys_backup_banner_accent_color" @@ -18,7 +18,7 @@ android:gravity="center|start" android:minHeight="80dp" android:padding="16dp" - app:drawableStartCompat="@drawable/error" + app:drawableStartCompat="@drawable/ic_warning_badge" tools:text="This room is continuation…" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/view_notification_area.xml b/vector/src/main/res/layout/view_notification_area.xml index 5e3a79291c..1d30b87aa4 100644 --- a/vector/src/main/res/layout/view_notification_area.xml +++ b/vector/src/main/res/layout/view_notification_area.xml @@ -1,38 +1,34 @@ - - - + android:orientation="horizontal" + android:padding="16dp"> + tools:src="@drawable/ic_warning_badge" /> - \ No newline at end of file + \ 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 54356db664..9305deb752 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -926,7 +926,7 @@ Resend unsent messages Delete unsent messages File not found - You do not have permission to post to this room + You do not have permission to post to this room. %d new message %d new messages @@ -1820,7 +1820,7 @@ Please enter a username. Please enter your password. - This room has been replaced and is no longer active + This room has been replaced and is no longer active. The conversation continues here This room is a continuation of another conversation Click here to see older messages @@ -2735,6 +2735,11 @@ Server file upload limit Your homeserver accepts attachments (files, media, etc.) with a size up to %s. The limit is unknown. + + Room Versions 🕶 + Default Version + stable + unstable No cryptographic information available @@ -3295,6 +3300,7 @@ Create a Space Join the Space with the given id Leave room with given id (or current room if null) + Upgrades a room to a new version Sending Sent @@ -3406,4 +3412,19 @@ "Teammate spaces aren’t quite ready but you can still give them a try" "At the moment people might not be able to join any private rooms you make.\n\nWe’ll be improving this as part of the beta, but just wanted to let you know." - + + Join replacement room + Please be patient, it may take some time. + + + Upgrade + Upgrade public room + Upgrade private room + Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.\nThis usually only affects how the room is processed on the server. + You\'ll upgrade this room from %s to %s. + Automatically invite users + Automatically update space parent + Automatically update parent space + You need permission to upgrade a room + + From 57c75f80399e0f4bb1ba24c9b08c0eede3fc5b26 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 24 Jun 2021 09:38:20 +0200 Subject: [PATCH 005/111] Ugrade unstable room notice in settings default update parent, clean migrate bottomsheet layout --- .../room/version/RoomVersionService.kt | 5 +- .../internal/session/room/RoomUpgradeBody.kt | 2 +- .../session/room/RoomUpgradeResponse.kt | 2 +- .../room/version/DefaultRoomVersionService.kt | 17 ++- .../room/version/RoomVersionUpgradeTask.kt | 2 +- tools/check/forbidden_strings_in_code.txt | 2 +- .../app/features/command/CommandParser.kt | 6 +- .../detail/upgrade/MigrateRoomBottomSheet.kt | 112 +++++++++------ .../detail/upgrade/MigrateRoomController.kt | 135 ------------------ .../detail/upgrade/MigrateRoomViewState.kt | 2 +- .../roomprofile/RoomProfileController.kt | 28 +++- .../roomprofile/RoomProfileFragment.kt | 6 + .../roomprofile/RoomProfileViewModel.kt | 12 +- .../roomprofile/RoomProfileViewState.kt | 6 +- .../res/layout/bottom_sheet_generic_list.xml | 1 + .../res/layout/bottom_sheet_room_upgrade.xml | 90 ++++++++++++ vector/src/main/res/values/strings.xml | 3 + 17 files changed, 239 insertions(+), 192 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt create mode 100644 vector/src/main/res/layout/bottom_sheet_room_upgrade.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt index a90e1343f2..08dd955394 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.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. @@ -26,7 +26,8 @@ interface RoomVersionService { */ suspend fun upgradeToVersion(version: String): String - suspend fun getRecommendedVersion() : String + fun getRecommendedVersion() : String fun userMayUpgradeRoom(userId: String): Boolean + fun isUsingUnstableRoomVersion(): Boolean } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt index feefbe5f66..4629f6e409 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.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/internal/session/room/RoomUpgradeResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.kt index 16e95194a3..1cca2c572b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.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/internal/session/room/version/DefaultRoomVersionService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt index 1b5d62926e..9d253e8b1c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.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. @@ -24,6 +24,7 @@ import io.realm.Realm 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.homeserver.RoomVersionStatus import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper @@ -63,7 +64,7 @@ internal class DefaultRoomVersionService @AssistedInject constructor( ) } - override suspend fun getRecommendedVersion(): String { + override fun getRecommendedVersion(): String { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> HomeServerCapabilitiesEntity.get(realm)?.let { HomeServerCapabilitiesMapper.map(it) @@ -71,6 +72,18 @@ internal class DefaultRoomVersionService @AssistedInject constructor( } } + override fun isUsingUnstableRoomVersion(): Boolean { + var isUsingUnstable: Boolean + Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val versionCaps = HomeServerCapabilitiesEntity.get(realm)?.let { + HomeServerCapabilitiesMapper.map(it) + }?.roomVersions + val currentVersion = getRoomVersion() + isUsingUnstable = versionCaps?.supportedVersion?.firstOrNull { it.version == currentVersion }?.status == RoomVersionStatus.UNSTABLE + } + return isUsingUnstable + } + override fun userMayUpgradeRoom(userId: String): Boolean { val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) ?.content?.toModel() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt index 444795b5a9..457bb3e948 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.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/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index e0645e00b3..ba9dec0877 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -162,7 +162,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===101 +enum class===102 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 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 224956049c..adba6e4a18 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 @@ -332,7 +332,11 @@ object CommandParser { } Command.UPGRADE_ROOM.command -> { val newVersion = textMessage.substring(Command.UPGRADE_ROOM.command.length).trim() - ParsedCommand.UpgradeRoom(newVersion) + if (newVersion.isEmpty()) { + ParsedCommand.ErrorSyntax(Command.UPGRADE_ROOM) + } else { + ParsedCommand.UpgradeRoom(newVersion) + } } else -> { // Unknown command diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt index 12a0fb222b..0de905d059 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt @@ -21,22 +21,24 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.fragment.app.setFragmentResult -import com.airbnb.epoxy.OnModelBuildFinishedListener +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success 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.cleanup -import im.vector.app.core.extensions.configureWith +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment -import im.vector.app.databinding.BottomSheetGenericListBinding +import im.vector.app.databinding.BottomSheetRoomUpgradeBinding import kotlinx.parcelize.Parcelize import javax.inject.Inject class MigrateRoomBottomSheet : - VectorBaseBottomSheetDialogFragment(), - MigrateRoomViewModel.Factory, MigrateRoomController.InteractionListener { + VectorBaseBottomSheetDialogFragment(), + MigrateRoomViewModel.Factory { @Parcelize data class Args( @@ -50,7 +52,7 @@ class MigrateRoomBottomSheet : override val showExpanded = true @Inject - lateinit var epoxyController: MigrateRoomController + lateinit var errorFormatter: ErrorFormatter val viewModel: MigrateRoomViewModel by fragmentViewModel() @@ -58,41 +60,79 @@ class MigrateRoomBottomSheet : injector.inject(this) } - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = - BottomSheetGenericListBinding.inflate(inflater, container, false) - override fun invalidate() = withState(viewModel) { state -> - epoxyController.setData(state) - super.invalidate() - when (val result = state.upgradingStatus) { + views.headerText.setText(if (state.isPublic) R.string.upgrade_public_room else R.string.upgrade_private_room) + views.upgradeFromTo.text = getString(R.string.upgrade_public_room_from_to, state.currentVersion, state.newVersion) + + views.autoInviteSwitch.isVisible = !state.isPublic && state.otherMemberCount > 0 + + views.autoUpdateParent.isVisible = state.knownParents.isNotEmpty() + + when (state.upgradingStatus) { + is Loading -> { + views.progressBar.isVisible = true + views.progressBar.isIndeterminate = state.upgradingProgressIndeterminate + views.progressBar.progress = state.upgradingProgress + views.progressBar.max = state.upgradingProgressTotal + views.inlineError.setTextOrHide(null) + views.button.isVisible = false + } is Success -> { - val result = result.invoke() - if (result is UpgradeRoomViewModelTask.Result.Success) { - setFragmentResult(REQUEST_KEY, Bundle().apply { - putString(BUNDLE_KEY_REPLACEMENT_ROOM, result.replacementRoomId) - }) - dismiss() + views.progressBar.isVisible = false + when (val result = state.upgradingStatus.invoke()) { + is UpgradeRoomViewModelTask.Result.Failure -> { + val errorText = when (result) { + is UpgradeRoomViewModelTask.Result.UnknownRoom -> { + // should not happen + getString(R.string.unknown_error) + } + is UpgradeRoomViewModelTask.Result.NotAllowed -> { + getString(R.string.upgrade_room_no_power_to_manage) + } + is UpgradeRoomViewModelTask.Result.ErrorFailure -> { + errorFormatter.toHumanReadable(result.throwable) + } + else -> null + } + views.inlineError.setTextOrHide(errorText) + views.button.isVisible = true + views.button.text = getString(R.string.global_retry) + } + is UpgradeRoomViewModelTask.Result.Success -> { + setFragmentResult(REQUEST_KEY, Bundle().apply { + putString(BUNDLE_KEY_REPLACEMENT_ROOM, result.replacementRoomId) + }) + dismiss() + } } } + else -> { + views.button.isVisible = true + views.button.text = getString(R.string.upgrade) + } } + + super.invalidate() } - val postBuild = OnModelBuildFinishedListener { - view?.post { forceExpandState() } - } + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + BottomSheetRoomUpgradeBinding.inflate(inflater, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - epoxyController.callback = this - views.bottomSheetRecyclerView.configureWith(epoxyController) - epoxyController.addModelBuildListener(postBuild) - } - override fun onDestroyView() { - views.bottomSheetRecyclerView.cleanup() - epoxyController.removeModelBuildListener(postBuild) - super.onDestroyView() + views.button.debouncedClicks { + viewModel.handle(MigrateRoomAction.UpgradeRoom) + } + + views.autoInviteSwitch.setOnCheckedChangeListener { _, isChecked -> + viewModel.handle(MigrateRoomAction.SetAutoInvite(isChecked)) + } + + views.autoUpdateParent.setOnCheckedChangeListener { _, isChecked -> + viewModel.handle(MigrateRoomAction.SetUpdateKnownParentSpace(isChecked)) + } } override fun create(initialState: MigrateRoomViewState): MigrateRoomViewModel { @@ -111,16 +151,4 @@ class MigrateRoomBottomSheet : } } } - - override fun onAutoInvite(autoInvite: Boolean) { - viewModel.handle(MigrateRoomAction.SetAutoInvite(autoInvite)) - } - - override fun onAutoUpdateParent(update: Boolean) { - viewModel.handle(MigrateRoomAction.SetUpdateKnownParentSpace(update)) - } - - override fun onConfirmUpgrade() { - viewModel.handle(MigrateRoomAction.UpgradeRoom) - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt deleted file mode 100644 index 8d037860d2..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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.home.room.detail.upgrade - -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.errorWithRetryItem -import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.resources.StringProvider -import im.vector.app.core.ui.bottomsheet.bottomSheetTitleItem -import im.vector.app.core.ui.list.ItemStyle -import im.vector.app.core.ui.list.genericFooterItem -import im.vector.app.core.ui.list.genericProgressBarItem -import im.vector.app.features.form.formSubmitButtonItem -import im.vector.app.features.form.formSwitchItem -import javax.inject.Inject - -class MigrateRoomController @Inject constructor( - private val stringProvider: StringProvider, - private val errorFormatter: ErrorFormatter -) : TypedEpoxyController() { - - interface InteractionListener { - fun onAutoInvite(autoInvite: Boolean) - fun onAutoUpdateParent(update: Boolean) - fun onConfirmUpgrade() - } - - var callback: InteractionListener? = null - - override fun buildModels(data: MigrateRoomViewState?) { - data ?: return - - val host = this@MigrateRoomController - - bottomSheetTitleItem { - id("title") - title( - host.stringProvider.getString(if (data.isPublic) R.string.upgrade_public_room else R.string.upgrade_private_room) - ) - } - - genericFooterItem { - id("warning_text") - centered(false) - style(ItemStyle.NORMAL_TEXT) - text(host.stringProvider.getString(R.string.upgrade_room_warning)) - } - - genericFooterItem { - id("from_to_room") - centered(false) - style(ItemStyle.NORMAL_TEXT) - text(host.stringProvider.getString(R.string.upgrade_public_room_from_to, data.currentVersion, data.newVersion)) - } - - if (!data.isPublic && data.otherMemberCount > 0) { - formSwitchItem { - id("auto_invite") - switchChecked(data.shouldIssueInvites) - title(host.stringProvider.getString(R.string.upgrade_room_auto_invite)) - listener { switch -> host.callback?.onAutoInvite(switch) } - } - } - - if (data.knownParents.isNotEmpty()) { - formSwitchItem { - id("update_parent") - switchChecked(data.shouldUpdateKnownParents) - title(host.stringProvider.getString(R.string.upgrade_room_update_parent)) - listener { switch -> host.callback?.onAutoUpdateParent(switch) } - } - } - when (data.upgradingStatus) { - is Loading -> { - genericProgressBarItem { - id("upgrade_progress") - indeterminate(data.upgradingProgressIndeterminate) - progress(data.upgradingProgress) - total(data.upgradingProgressTotal) - } - } - is Success -> { - when (val result = data.upgradingStatus.invoke()) { - is UpgradeRoomViewModelTask.Result.Failure -> { - val errorText = when (result) { - is UpgradeRoomViewModelTask.Result.UnknownRoom -> { - // should not happen - host.stringProvider.getString(R.string.unknown_error) - } - is UpgradeRoomViewModelTask.Result.NotAllowed -> { - host.stringProvider.getString(R.string.upgrade_room_no_power_to_manage) - } - is UpgradeRoomViewModelTask.Result.ErrorFailure -> { - host.errorFormatter.toHumanReadable(result.throwable) - } - else -> null - } - errorWithRetryItem { - id("error") - text(errorText) - listener { host.callback?.onConfirmUpgrade() } - } - } - is UpgradeRoomViewModelTask.Result.Success -> { - // nop, dismisses - } - } - } - else -> { - formSubmitButtonItem { - id("migrate") - buttonTitleId(R.string.upgrade) - buttonClickListener { host.callback?.onConfirmUpgrade() } - } - } - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt index 78c280fb10..e3936de42f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt @@ -26,7 +26,7 @@ data class MigrateRoomViewState( val currentVersion: String? = null, val isPublic: Boolean = false, val shouldIssueInvites: Boolean = false, - val shouldUpdateKnownParents: Boolean = false, + val shouldUpdateKnownParents: Boolean = true, val otherMemberCount: Int = 0, val knownParents: List = emptyList(), val upgradingStatus: Async = Uninitialized, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index aa2db348c8..106062f7b3 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -22,8 +22,10 @@ import im.vector.app.R import im.vector.app.core.epoxy.expandableTextItem import im.vector.app.core.epoxy.profiles.buildProfileAction import im.vector.app.core.epoxy.profiles.buildProfileSection +import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.core.ui.list.genericPositiveButtonItem import im.vector.app.features.home.ShortcutCreator import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod @@ -34,6 +36,7 @@ import javax.inject.Inject class RoomProfileController @Inject constructor( private val stringProvider: StringProvider, + private val colorProvider: ColorProvider, private val vectorPreferences: VectorPreferences, private val shortcutCreator: ShortcutCreator ) : TypedEpoxyController() { @@ -55,6 +58,7 @@ class RoomProfileController @Inject constructor( fun onRoomIdClicked() fun onRoomDevToolsClicked() fun onUrlInTopicLongClicked(url: String) + fun doMigrateToVersion(newVersion: String) } override fun buildModels(data: RoomProfileViewState?) { @@ -87,6 +91,28 @@ class RoomProfileController @Inject constructor( // Security buildProfileSection(stringProvider.getString(R.string.room_profile_section_security)) + + // Upgrade warning + val roomVersion = data.roomCreateContent()?.roomVersion + if (data.canUpgradeRoom + && !data.isTombstoned + && roomVersion != null + && data.isUsingUnstableRoomVersion + && data.recommendedRoomVersion != null) { + genericFooterItem { + id("version_warning") + text(host.stringProvider.getString(R.string.room_using_unstable_room_version, roomVersion)) + textColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) + centered(false) + } + + genericPositiveButtonItem { + id("migrate_button") + text(host.stringProvider.getString(R.string.room_upgrade_to_recommened_version)) + buttonClickAction { host.callback?.doMigrateToVersion(data.recommendedRoomVersion) } + } + } + val learnMoreSubtitle = if (roomSummary.isEncrypted) { if (roomSummary.isDirect) R.string.direct_room_profile_encrypted_subtitle else R.string.room_profile_encrypted_subtitle } else { @@ -194,7 +220,7 @@ class RoomProfileController @Inject constructor( editable = false, action = { callback?.onRoomIdClicked() } ) - data.roomCreateContent()?.roomVersion?.let { + roomVersion?.let { buildProfileAction( id = "roomVersion", title = stringProvider.getString(R.string.room_settings_room_version_title), diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index f9324ad3e2..8a10bcc842 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -45,6 +45,7 @@ import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.FragmentMatrixProfileBinding import im.vector.app.databinding.ViewStubRoomProfileHeaderBinding import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.list.actions.RoomListActionsArgs import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction @@ -303,6 +304,11 @@ class RoomProfileFragment @Inject constructor( copyToClipboard(requireContext(), url, true) } + override fun doMigrateToVersion(newVersion: String) { + MigrateRoomBottomSheet.newInstance(roomProfileArgs.roomId, newVersion) + .show(parentFragmentManager, "migrate") + } + private fun onShareRoomProfile(permalink: String) { startSharePlainTextIntent( fragment = this, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt index 209ebcc35b..9fee87880a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt @@ -22,8 +22,8 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory 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.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -81,8 +81,14 @@ class RoomProfileViewModel @AssistedInject constructor( rxRoom.liveStateEvent(EventType.STATE_ROOM_CREATE, QueryStringValue.NoCondition) .mapOptional { it.content.toModel() } .unwrap() - .execute { - copy(roomCreateContent = it) + .execute { async -> + copy( + roomCreateContent = async, + recommendedRoomVersion = room.getRecommendedVersion(), + isUsingUnstableRoomVersion = room.isUsingUnstableRoomVersion(), + canUpgradeRoom = room.userMayUpgradeRoom(session.myUserId), + isTombstoned = room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE) != null + ) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt index bf7cd732ef..999b6540bd 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt @@ -30,7 +30,11 @@ data class RoomProfileViewState( val roomCreateContent: Async = Uninitialized, val bannedMembership: Async> = Uninitialized, val actionPermissions: ActionPermissions = ActionPermissions(), - val isLoading: Boolean = false + val isLoading: Boolean = false, + val isUsingUnstableRoomVersion: Boolean = false, + val recommendedRoomVersion: String? = null, + val canUpgradeRoom: Boolean = false, + val isTombstoned: Boolean = false ) : MvRxState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/res/layout/bottom_sheet_generic_list.xml b/vector/src/main/res/layout/bottom_sheet_generic_list.xml index 87a2cb54fc..966ad48b25 100644 --- a/vector/src/main/res/layout/bottom_sheet_generic_list.xml +++ b/vector/src/main/res/layout/bottom_sheet_generic_list.xml @@ -6,6 +6,7 @@ android:layout_height="match_parent" android:background="?colorSurface" android:fadeScrollbars="false" + android:nestedScrollingEnabled="false" android:scrollbars="vertical" tools:itemCount="5" tools:listitem="@layout/item_bottom_sheet_action" /> diff --git a/vector/src/main/res/layout/bottom_sheet_room_upgrade.xml b/vector/src/main/res/layout/bottom_sheet_room_upgrade.xml new file mode 100644 index 0000000000..3cb1daffad --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_room_upgrade.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + +