Merge pull request #3551 from vector-im/feature/bca/room_upgrade

Feature/bca/room upgrade
This commit is contained in:
Benoit Marty 2021-07-08 10:00:01 +02:00 committed by GitHub
commit 2948f03978
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 1390 additions and 104 deletions

1
changelog.d/3551.feature Normal file
View file

@ -0,0 +1 @@
Room version capabilities and room upgrade support, better error feedback

View file

@ -32,7 +32,13 @@ data class HomeServerCapabilities(
/**
* Default identity server url, provided in Wellknown
*/
val defaultIdentityServerUrl: String? = null
val defaultIdentityServerUrl: String? = null,
/**
* Room versions supported by the server
* 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.
*/
val roomVersions: RoomVersionCapabilities? = null
) {
companion object {
const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L

View file

@ -0,0 +1,32 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.homeserver
data class RoomVersionCapabilities(
val defaultRoomVersion: String,
val supportedVersion: List<RoomVersionInfo>
)
data class RoomVersionInfo(
val version: String,
val status: RoomVersionStatus
)
enum class RoomVersionStatus {
STABLE,
UNSTABLE
}

View file

@ -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

View file

@ -0,0 +1,45 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.version
interface RoomVersionService {
/**
* Return the room version of this room
*/
fun getRoomVersion(): String
/**
* Upgrade to the given room version
* @return the replacement room id
*/
suspend fun upgradeToVersion(version: String): String
/**
* Get the recommended room version for the current homeserver
*/
fun getRecommendedVersion() : String
/**
* Ask if the user has enough power level to upgrade the room
*/
fun userMayUpgradeRoom(userId: String): Boolean
/**
* Return true if the current room version is declared unstable by the homeserver
*/
fun isUsingUnstableRoomVersion(): Boolean
}

View file

@ -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

View file

@ -46,7 +46,7 @@ import timber.log.Timber
internal object RealmSessionStoreMigration : RealmMigration {
const val SESSION_STORE_SCHEMA_VERSION = 15L
const val SESSION_STORE_SCHEMA_VERSION = 16L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
@ -66,6 +66,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
if (oldVersion <= 12) migrateTo13(realm)
if (oldVersion <= 13) migrateTo14(realm)
if (oldVersion <= 14) migrateTo15(realm)
if (oldVersion <= 15) migrateTo16(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
@ -308,6 +309,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
}
private fun migrateTo15(realm: DynamicRealm) {
Timber.d("Step 14 -> 15")
// fix issue with flattenParentIds on DM that kept growing with duplicate
// so we reset it, will be updated next sync
realm.where("RoomSummaryEntity")
@ -318,4 +320,14 @@ internal object RealmSessionStoreMigration : RealmMigration {
it.setString(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, null)
}
}
private fun migrateTo16(realm: DynamicRealm) {
Timber.d("Step 15 -> 16")
realm.schema.get("HomeServerCapabilitiesEntity")
?.addField(HomeServerCapabilitiesEntityFields.ROOM_VERSIONS_JSON, String::class.java)
?.transform { obj ->
// Schedule a refresh of the capabilities
obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
}
}
}

View file

@ -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,30 @@ internal object HomeServerCapabilitiesMapper {
canChangePassword = entity.canChangePassword,
maxUploadFileSize = entity.maxUploadFileSize,
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
defaultIdentityServerUrl = entity.defaultIdentityServerUrl
defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
roomVersions = mapRoomVersion(entity.roomVersionsJson)
)
}
private fun mapRoomVersion(roomVersionsJson: String?): RoomVersionCapabilities? {
roomVersionsJson ?: return null
return tryOrNull {
MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).fromJson(roomVersionsJson)?.let {
RoomVersionCapabilities(
defaultRoomVersion = it.default ?: DefaultRoomVersionService.DEFAULT_ROOM_VERSION,
supportedVersion = it.available.entries.map { entry ->
RoomVersionInfo(
version = entry.key,
status = if (entry.value == "stable") {
RoomVersionStatus.STABLE
} else {
RoomVersionStatus.UNSTABLE
}
)
}
)
}
}
}
}

View file

@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
internal open class HomeServerCapabilitiesEntity(
var canChangePassword: Boolean = true,
var roomVersionsJson: String? = null,
var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
var lastVersionIdentityServerSupported: Boolean = false,
var defaultIdentityServerUrl: String? = null,

View file

@ -16,18 +16,12 @@
package org.matrix.android.sdk.internal.session.homeserver
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
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 javax.inject.Inject
internal class DefaultHomeServerCapabilitiesService @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask
) : HomeServerCapabilitiesService {
@ -36,11 +30,7 @@ internal class DefaultHomeServerCapabilitiesService @Inject constructor(
}
override fun getHomeServerCapabilities(): HomeServerCapabilities {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
HomeServerCapabilitiesEntity.get(realm)?.let {
HomeServerCapabilitiesMapper.map(it)
}
}
return homeServerCapabilitiesDataSource.getHomeServerCapabilities()
?: HomeServerCapabilities()
}
}

View file

@ -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,21 @@ internal data class ChangePassword(
val enabled: Boolean?
)
@JsonClass(generateAdapter = true)
internal data class RoomVersions(
/**
* Required. The default room version the server is using for new rooms.
*/
@Json(name = "default")
val default: String?,
/**
* Required. A detailed description of the room versions the server supports.
*/
@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()

View file

@ -25,6 +25,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
@ -108,6 +109,10 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
if (getCapabilitiesResult != null) {
homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
homeServerCapabilitiesEntity.roomVersionsJson = getCapabilitiesResult.capabilities?.roomVersions?.let {
MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
}
}
if (getMediaConfigResult != null) {

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.homeserver
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
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 javax.inject.Inject
internal class HomeServerCapabilitiesDataSource @Inject constructor(
@SessionDatabase private val monarchy: Monarchy
) {
fun getHomeServerCapabilities(): HomeServerCapabilities? {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
HomeServerCapabilitiesEntity.get(realm)?.let {
HomeServerCapabilitiesMapper.map(it)
}
}
}
}

View file

@ -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
@ -67,9 +68,11 @@ internal class DefaultRoom(override val roomId: String,
private val roomMembersService: MembershipService,
private val roomPushRuleService: RoomPushRuleService,
private val roomAccountDataService: RoomAccountDataService,
private val roomVersionService: RoomVersionService,
private val sendStateTask: SendStateTask,
private val viaParameterFinder: ViaParameterFinder,
private val searchTask: SearchTask) :
private val searchTask: SearchTask
) :
Room,
TimelineService by timelineService,
SendService by sendService,
@ -85,7 +88,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<Optional<RoomSummary>> {
return roomSummaryDataSource.getRoomSummaryLive(roomId)

View file

@ -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
}

View file

@ -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,
@ -87,6 +89,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
roomMembersService = membershipServiceFactory.create(roomId),
roomPushRuleService = roomPushRuleServiceFactory.create(roomId),
roomAccountDataService = roomAccountDataServiceFactory.create(roomId),
roomVersionService = roomVersionServiceFactory.create(roomId),
sendStateTask = sendStateTask,
searchTask = searchTask,
viaParameterFinder = viaParameterFinder

View file

@ -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
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class RoomUpgradeBody(
@Json(name = "new_version")
val newVersion: String
)

View file

@ -0,0 +1,26 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class RoomUpgradeResponse(
@Json(name = "replacement_room")
val replacementRoomId: String
)

View file

@ -0,0 +1,84 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.version
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
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
import org.matrix.android.sdk.api.session.room.version.RoomVersionService
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
internal class DefaultRoomVersionService @AssistedInject constructor(
@Assisted private val roomId: String,
private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
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<RoomCreateContent>()
?.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 = roomId,
newVersion = version
)
)
}
override fun getRecommendedVersion(): String {
return homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.roomVersions?.defaultRoomVersion ?: DEFAULT_ROOM_VERSION
}
override fun isUsingUnstableRoomVersion(): Boolean {
val versionCaps = homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.roomVersions
val currentVersion = getRoomVersion()
return versionCaps?.supportedVersion?.firstOrNull { it.version == currentVersion }?.status == RoomVersionStatus.UNSTABLE
}
override fun userMayUpgradeRoom(userId: String): Boolean {
val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
?.content?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
return powerLevelsHelper?.isUserAllowedToSend(userId, true, EventType.STATE_ROOM_TOMBSTONE) ?: false
}
companion object {
const val DEFAULT_ROOM_VERSION = "1"
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.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<RoomVersionUpgradeTask.Params, String> {
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
}
}

View file

@ -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<SpaceChildContent>()
}
override suspend fun setChildrenOrder(roomId: String, order: String?) {
val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
.firstOrNull()

View file

@ -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

View file

@ -162,7 +162,6 @@ class VectorApplication :
// Do not display the name change popup
doNotShowDisclaimerDialog(this)
}
if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession)

View file

@ -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
@ -193,6 +195,8 @@ interface ScreenComponent {
fun inject(bottomSheet: SpaceSettingsMenuBottomSheet)
fun inject(bottomSheet: InviteRoomSpaceChooserBottomSheet)
fun inject(bottomSheet: SpaceInviteBottomSheet)
fun inject(bottomSheet: JoinReplacementRoomBottomSheet)
fun inject(bottomSheet: MigrateRoomBottomSheet)
/* ==========================================================================================
* Others

View file

@ -46,7 +46,7 @@ import java.util.concurrent.TimeUnit
/**
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)
*/
abstract class VectorBaseBottomSheetDialogFragment<VB: ViewBinding> : BottomSheetDialogFragment(), MvRxView {
abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomSheetDialogFragment(), MvRxView {
private val mvrxViewIdProperty = MvRxViewId()
final override val mvrxViewId: String by mvrxViewIdProperty
@ -168,6 +168,10 @@ abstract class VectorBaseBottomSheetDialogFragment<VB: ViewBinding> : BottomShee
@CallSuper
override fun invalidate() {
forceExpandState()
}
protected fun forceExpandState() {
if (showExpanded) {
// Force the bottom sheet to be expanded
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED

View file

@ -0,0 +1,50 @@
/*
* Copyright 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.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 progress bar item.
*/
@EpoxyModelClass(layout = R.layout.item_generic_progress)
abstract class GenericProgressBarItem : VectorEpoxyModel<GenericProgressBarItem.Holder>() {
@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<ProgressBar>(R.id.genericProgressBar)
}
}

View file

@ -21,11 +21,12 @@ 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
import im.vector.app.core.error.ResourceLimitErrorFormatter
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.databinding.ViewNotificationAreaBinding
import im.vector.app.features.themes.ThemeUtils
@ -44,7 +45,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
@ -69,12 +70,13 @@ class NotificationAreaView @JvmOverloads constructor(
cleanUp()
state = newState
when (newState) {
State.Initial -> Unit
is State.Default -> renderDefault()
is State.Hidden -> renderHidden()
is State.NoPermissionToPost -> renderNoPermissionToPost()
is State.Tombstone -> renderTombstone(newState)
is State.Tombstone -> renderTombstone()
is State.ResourceLimitExceededError -> renderResourceLimitExceededError(newState)
}
}.exhaustive
}
// PRIVATE METHODS ****************************************************************************************************************************************
@ -125,15 +127,15 @@ class NotificationAreaView @JvmOverloads constructor(
setBackgroundColor(ContextCompat.getColor(context, backgroundColor))
}
private fun renderTombstone(state: State.Tombstone) {
private fun renderTombstone() {
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"
span(resources.getString(R.string.room_tombstone_continuation_link)) {
textDecorationLine = "underline"
onClick = { delegate?.onTombstoneEventClicked(state.tombstoneEvent) }
onClick = { delegate?.onTombstoneEventClicked() }
}
}
views.roomNotificationMessage.movementMethod = BetterLinkMovementMethod.getInstance()
@ -177,6 +179,6 @@ class NotificationAreaView @JvmOverloads constructor(
* An interface to delegate some actions to another object
*/
interface Delegate {
fun onTombstoneEventClicked(tombstoneEvent: Event)
fun onTombstoneEventClicked()
}
}

View file

@ -50,7 +50,8 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
CREATE_SPACE("/createspace", "<name> <invitee>*", 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", "<roomId?>", R.string.command_description_leave_room, true);
LEAVE_ROOM("/leave", "<roomId?>", R.string.command_description_leave_room, true),
UPGRADE_ROOM("/upgraderoom", "newVersion", R.string.command_description_upgrade_room, true);
val length
get() = command.length + 1

View file

@ -312,24 +312,32 @@ 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()
if (newVersion.isEmpty()) {
ParsedCommand.ErrorSyntax(Command.UPGRADE_ROOM)
} else {
ParsedCommand.UpgradeRoom(newVersion)
}
}
else -> {
// Unknown command
ParsedCommand.ErrorUnknownSlashCommand(slashCommand)

View file

@ -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()
}

View file

@ -0,0 +1,84 @@
/*
* 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 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<BottomSheetTombstoneJoinBinding>() {
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) {
viewModel.handle(RoomDetailAction.JoinAndOpenReplacementRoom)
}
}
viewModel.selectSubscribe(this, RoomDetailViewState::joinUpgradedRoomAsync) { joinState ->
when (joinState) {
// it should never be Uninitialized
Uninitialized,
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)
}
}
}
}
}

View file

@ -20,7 +20,6 @@ import android.net.Uri
import android.view.View
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.timeline.Timeline
@ -44,7 +43,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailAction()
object MarkAllAsRead : RoomDetailAction()
data class DownloadOrOpen(val eventId: String, val senderId: String?, val messageFileContent: MessageWithAttachmentContent) : RoomDetailAction()
data class HandleTombstoneEvent(val event: Event) : RoomDetailAction()
object JoinAndOpenReplacementRoom : RoomDetailAction()
object AcceptInvite : RoomDetailAction()
object RejectInvite : RoomDetailAction()
@ -108,4 +107,5 @@ sealed class RoomDetailAction : VectorViewModelAction {
// Failed messages
object RemoveAllFailedMessages : RoomDetailAction()
data class RoomUpgradeSuccess(val replacementRoomId: String): RoomDetailAction()
}

View file

@ -50,6 +50,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
@ -59,11 +60,7 @@ import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.epoxy.addGlidePreloader
import com.airbnb.epoxy.glidePreloader
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -144,6 +141,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
@ -177,7 +175,6 @@ import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -302,6 +299,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)
@ -348,10 +354,6 @@ class RoomDetailFragment @Inject constructor(
invalidateOptionsMenu()
})
roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
renderTombstoneEventHandling(it)
}
roomDetailViewModel.selectSubscribe(RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ ->
updateJumpToReadMarkerViewVisibility()
}
@ -405,6 +407,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 +427,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 -> {
@ -472,6 +489,9 @@ class RoomDetailFragment @Inject constructor(
private fun handleOpenRoom(openRoom: RoomDetailViewEvents.OpenRoom) {
navigator.openRoom(requireContext(), openRoom.roomId, null)
if (openRoom.closeCurrentRoom) {
requireActivity().finish()
}
}
private fun requestNativeWidgetPermission(it: RoomDetailViewEvents.RequestNativeWidgetPermission) {
@ -776,8 +796,8 @@ class RoomDetailFragment @Inject constructor(
private fun setupNotificationView() {
views.notificationAreaView.delegate = object : NotificationAreaView.Delegate {
override fun onTombstoneEventClicked(tombstoneEvent: Event) {
roomDetailViewModel.handle(RoomDetailAction.HandleTombstoneEvent(tombstoneEvent))
override fun onTombstoneEventClicked() {
roomDetailViewModel.handle(RoomDetailAction.JoinAndOpenReplacementRoom)
}
}
}
@ -964,6 +984,8 @@ class RoomDetailFragment @Inject constructor(
insertUserDisplayNameInTextEditor(roomDetailPendingAction.userId)
is RoomDetailPendingAction.OpenOrCreateDm ->
roomDetailViewModel.handle(RoomDetailAction.OpenOrCreateDm(roomDetailPendingAction.userId))
is RoomDetailPendingAction.OpenRoom ->
handleOpenRoom(RoomDetailViewEvents.OpenRoom(roomDetailPendingAction.roomId, roomDetailPendingAction.closeCurrentRoom))
}.exhaustive
}
@ -1312,23 +1334,6 @@ class RoomDetailFragment @Inject constructor(
}
}
private fun renderTombstoneEventHandling(async: Async<String>) {
when (async) {
is Loading -> {
// TODO Better handling progress
vectorBaseActivity.showWaitingView(getString(R.string.joining_room))
}
is Success -> {
navigator.openRoom(vectorBaseActivity, async())
vectorBaseActivity.finish()
}
is Fail -> {
vectorBaseActivity.hideWaitingView()
vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error))
}
}
}
private fun renderSendMessageResult(sendMessageResult: RoomDetailViewEvents.SendMessageResult) {
when (sendMessageResult) {
is RoomDetailViewEvents.SlashCommandHandled -> {

View file

@ -20,4 +20,5 @@ sealed class RoomDetailPendingAction {
data class OpenOrCreateDm(val userId: String) : RoomDetailPendingAction()
data class JumpToReadReceipt(val userId: String) : RoomDetailPendingAction()
data class MentionUser(val userId: String) : RoomDetailPendingAction()
data class OpenRoom(val roomId: String, val closeCurrentRoom: Boolean = false) : RoomDetailPendingAction()
}

View file

@ -41,7 +41,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class ShowInfoOkDialog(val message: String) : RoomDetailViewEvents()
data class ShowE2EErrorMessage(val withHeldCode: WithHeldCode?) : RoomDetailViewEvents()
data class OpenRoom(val roomId: String) : RoomDetailViewEvents()
data class OpenRoom(val roomId: String, val closeCurrentRoom: Boolean = false) : RoomDetailViewEvents()
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents()
@ -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()
}

View file

@ -281,7 +281,7 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
is RoomDetailAction.JoinAndOpenReplacementRoom -> handleJoinAndOpenReplacementRoom()
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
@ -320,6 +320,12 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages()
RoomDetailAction.ResendAll -> handleResendAll()
is RoomDetailAction.RoomUpgradeSuccess -> {
setState {
copy(joinUpgradedRoomAsync = Success(action.replacementRoomId))
}
_viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true))
}
}.exhaustive
}
@ -573,24 +579,33 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}
private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) {
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>() ?: return
private fun handleJoinAndOpenReplacementRoom() = withState { state ->
val tombstoneContent = state.tombstoneEvent?.getClearContent()?.toModel<RoomTombstoneContent>() ?: return@withState
val roomId = tombstoneContent.replacementRoomId ?: ""
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
if (isRoomJoined) {
setState { copy(tombstoneEventHandling = Success(roomId)) }
setState { copy(joinUpgradedRoomAsync = Success(roomId)) }
_viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId, closeCurrentRoom = true))
} else {
val viaServers = MatrixPatterns.extractServerNameFromId(action.event.senderId)
val viaServers = MatrixPatterns.extractServerNameFromId(state.tombstoneEvent.senderId)
?.let { listOf(it) }
.orEmpty()
// need to provide feedback as joining could take some time
_viewEvents.post(RoomDetailViewEvents.RoomReplacementStarted)
setState {
copy(joinUpgradedRoomAsync = Loading())
}
viewModelScope.launch {
val result = runCatchingToAsync {
session.joinRoom(roomId, viaServers = viaServers)
roomId
}
setState {
copy(tombstoneEventHandling = result)
copy(joinUpgradedRoomAsync = result)
}
if (result is Success) {
_viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId, closeCurrentRoom = true))
}
}
}
@ -816,6 +831,16 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.UpgradeRoom -> {
_viewEvents.post(
RoomDetailViewEvents.ShowRoomUpgradeDialog(
slashCommandResult.newVersion,
room.roomSummary()?.isPublic ?: false
)
)
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
}.exhaustive
}
is SendMode.EDIT -> {

View file

@ -65,7 +65,7 @@ data class RoomDetailViewState(
val typingMessage: String? = null,
val sendMode: SendMode = SendMode.REGULAR("", false),
val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async<String> = Uninitialized,
val joinUpgradedRoomAsync: Async<String> = Uninitialized,
val syncState: SyncState = SyncState.Idle,
val highlightedEventId: String? = null,
val unreadState: UnreadState = UnreadState.Unknown,

View file

@ -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()
}

View file

@ -0,0 +1,152 @@
/*
* 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.core.view.isVisible
import androidx.fragment.app.setFragmentResult
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.error.ErrorFormatter
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.databinding.BottomSheetRoomUpgradeBinding
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
class MigrateRoomBottomSheet :
VectorBaseBottomSheetDialogFragment<BottomSheetRoomUpgradeBinding>(),
MigrateRoomViewModel.Factory {
@Parcelize
data class Args(
val roomId: String,
val newVersion: String
) : Parcelable
@Inject
lateinit var viewModelFactory: MigrateRoomViewModel.Factory
override val showExpanded = true
@Inject
lateinit var errorFormatter: ErrorFormatter
val viewModel: MigrateRoomViewModel by fragmentViewModel()
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun invalidate() = withState(viewModel) { state ->
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 -> {
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()
}
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
BottomSheetRoomUpgradeBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
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 {
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))
}
}
}
}

View file

@ -0,0 +1,114 @@
/*
* 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<MigrateRoomViewState, MigrateRoomAction, EmptyViewEvents>(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<MigrateRoomViewModel, MigrateRoomViewState> {
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()
}
}
}
private fun handleUpgradeRoom() = 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 = { indeterminate, progress, total ->
setState {
copy(
upgradingProgress = progress,
upgradingProgressTotal = total,
upgradingProgressIndeterminate = indeterminate
)
}
}
))
setState {
copy(upgradingStatus = Success(result))
}
}
}
}

View file

@ -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 = true,
val otherMemberCount: Int = 0,
val knownParents: List<String> = emptyList(),
val upgradingStatus: Async<UpgradeRoomViewModelTask.Result> = 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
)
}

View file

@ -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<UpgradeRoomViewModelTask.Params, UpgradeRoomViewModelTask.Result> {
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<String> = emptyList(),
val parentSpaceToUpdate: List<String> = 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)
}
}

View file

@ -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<RoomProfileViewState>() {
@ -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_recommended_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),

View file

@ -25,6 +25,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.isVisible
import androidx.fragment.app.setFragmentResultListener
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -43,6 +44,9 @@ 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.RoomDetailPendingAction
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
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
@ -61,6 +65,7 @@ data class RoomProfileArgs(
class RoomProfileFragment @Inject constructor(
private val roomProfileController: RoomProfileController,
private val avatarRenderer: AvatarRenderer,
private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
val roomProfileViewModelFactory: RoomProfileViewModel.Factory
) :
VectorBaseFragment<FragmentMatrixProfileBinding>(),
@ -81,6 +86,16 @@ class RoomProfileFragment @Inject constructor(
override fun getMenuRes() = R.menu.vector_room_profile
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle ->
bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId ->
roomDetailPendingActionStore.data = RoomDetailPendingAction.OpenRoom(replacementRoomId, closeCurrentRoom = true)
vectorBaseActivity.finish()
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
roomListQuickActionsSharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
@ -296,6 +311,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,

View file

@ -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,15 @@ class RoomProfileViewModel @AssistedInject constructor(
rxRoom.liveStateEvent(EventType.STATE_ROOM_CREATE, QueryStringValue.NoCondition)
.mapOptional { it.content.toModel<RoomCreateContent>() }
.unwrap()
.execute {
copy(roomCreateContent = it)
.execute { async ->
copy(
roomCreateContent = async,
// This is a shortcut, we should do the next lines elsewhere, but keep it like that for the moment.
recommendedRoomVersion = room.getRecommendedVersion(),
isUsingUnstableRoomVersion = room.isUsingUnstableRoomVersion(),
canUpgradeRoom = room.userMayUpgradeRoom(session.myUserId),
isTombstoned = room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE) != null
)
}
}

View file

@ -30,7 +30,11 @@ data class RoomProfileViewState(
val roomCreateContent: Async<RoomCreateContent> = Uninitialized,
val bannedMembership: Async<List<RoomMemberSummary>> = 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)

View file

@ -26,12 +26,14 @@ 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(
@ -130,5 +132,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
}
)
)
}
}
}
}
}
}

View file

@ -39,7 +39,7 @@
<ProgressBar
android:id="@+id/bug_report_progress_view"
style="?android:attr/progressBarStyleHorizontal"
style="@style/Widget.Vector.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_gravity="center_vertical"

View file

@ -12,7 +12,7 @@
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
style="@style/Widget.Vector.ProgressBar.Horizontal"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_marginTop="160dp"

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/headerText"
style="@style/Widget.Vector.TextView.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="12dp"
android:gravity="start"
android:textColor="?vctr_content_primary"
android:textStyle="bold"
tools:text="@string/upgrade_public_room" />
<TextView
android:id="@+id/descriptionText"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:gravity="start"
android:text="@string/upgrade_room_warning"
android:textColor="?vctr_content_secondary" />
<TextView
android:id="@+id/upgradeFromTo"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:gravity="start"
android:textColor="?vctr_content_secondary"
tools:text="@string/upgrade_public_room_from_to" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/autoInviteSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/upgrade_room_auto_invite"
tools:checked="true"
tools:visibility="visible" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/autoUpdateParent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/upgrade_room_update_parent_space"
tools:visibility="visible" />
<Button
android:id="@+id/button"
style="@style/Widget.Vector.Button.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/upgrade"
android:textAllCaps="true"
android:textColor="?colorSecondary" />
<ProgressBar
android:id="@+id/progressBar"
style="@style/Widget.Vector.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/inlineError"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:gravity="start"
android:textColor="?colorError"
android:visibility="gone"
tools:text="@string/unknown_error"
tools:visibility="visible" />
</LinearLayout>

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/headerText"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="12dp"
android:gravity="center"
android:text="@string/joining_replacement_room"
android:textColor="?vctr_content_primary"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/descriptionText"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:gravity="center"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/joinInfoHelpText"
app:layout_constraintTop_toBottomOf="@id/headerText"
app:layout_constraintVertical_bias="1"
tools:text="@string/it_may_take_some_time" />
<im.vector.app.core.platform.ButtonStateView
android:id="@+id/roomUpgradeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
app:bsv_button_text="@string/join"
app:bsv_loaded_image_src="@drawable/ic_tick"
app:bsv_use_flat_button="true" />
</LinearLayout>

View file

@ -17,7 +17,7 @@
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
style="@style/Widget.Vector.ProgressBar.Horizontal"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin_big"

View file

@ -0,0 +1,8 @@
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/genericProgressBar"
style="@style/Widget.Vector.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
tools:progress="30" />

View file

@ -10,7 +10,6 @@
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:background="?vctr_keys_backup_banner_accent_color"
@ -18,7 +17,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…" />
</FrameLayout>

View file

@ -17,7 +17,7 @@
<ProgressBar
android:id="@+id/mediaProgressBar"
style="?android:attr/progressBarStyleHorizontal"
style="@style/Widget.Vector.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_gravity="center_vertical"

View file

@ -56,7 +56,7 @@
<ProgressBar
android:id="@+id/waitingHorizontalProgress"
style="?android:attr/progressBarStyleHorizontal"
style="@style/Widget.Vector.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_marginTop="10dp"

View file

@ -1,38 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?vctr_keys_backup_banner_accent_color"
android:minHeight="48dp"
tools:parentTag="android.widget.RelativeLayout">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?vctr_list_separator" />
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:id="@+id/roomNotificationIcon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"
android:layout_marginStart="24dp"
android:layout_gravity="top"
android:importantForAccessibility="no"
android:padding="5dp"
tools:src="@drawable/error" />
tools:src="@drawable/ic_warning_badge" />
<TextView
android:id="@+id/roomNotificationMessage"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="64dp"
android:layout_marginEnd="16dp"
android:layout_gravity="center_vertical"
android:accessibilityLiveRegion="polite"
android:gravity="center"
android:gravity="start"
android:paddingStart="4dp"
android:paddingEnd="0dp"
android:textColor="?vctr_content_primary"
tools:text="@string/room_do_not_have_permission_to_post" />
</merge>
</LinearLayout>

View file

@ -927,7 +927,7 @@
<string name="room_resend_unsent_messages">Resend unsent messages</string>
<string name="room_delete_unsent_messages">Delete unsent messages</string>
<string name="room_message_file_not_found">File not found</string>
<string name="room_do_not_have_permission_to_post">You do not have permission to post to this room</string>
<string name="room_do_not_have_permission_to_post">You do not have permission to post to this room.</string>
<plurals name="room_new_messages_notification">
<item quantity="one">%d new message</item>
<item quantity="other">%d new messages</item>
@ -1821,7 +1821,7 @@
<string name="error_empty_field_enter_user_name">Please enter a username.</string>
<string name="error_empty_field_your_password">Please enter your password.</string>
<string name="room_tombstone_versioned_description">This room has been replaced and is no longer active</string>
<string name="room_tombstone_versioned_description">This room has been replaced and is no longer active.</string>
<string name="room_tombstone_continuation_link">The conversation continues here</string>
<string name="room_tombstone_continuation_description">This room is a continuation of another conversation</string>
<string name="room_tombstone_predecessor_link">Click here to see older messages</string>
@ -2739,6 +2739,11 @@
<string name="settings_server_upload_size_title">Server file upload limit</string>
<string name="settings_server_upload_size_content">Your homeserver accepts attachments (files, media, etc.) with a size up to %s.</string>
<string name="settings_server_upload_size_unknown">The limit is unknown.</string>
<!-- Please use the same emoji in the translations -->
<string name="settings_server_room_versions">Room Versions 👓</string>
<string name="settings_server_default_room_version">Default Version</string>
<string name="settings_server_room_version_stable">stable</string>
<string name="settings_server_room_version_unstable">unstable</string>
<string name="settings_failed_to_get_crypto_device_info">No cryptographic information available</string>
@ -3299,6 +3304,7 @@
<string name="command_description_create_space">Create a Space</string>
<string name="command_description_join_space">Join the Space with the given id</string>
<string name="command_description_leave_room">Leave room with given id (or current room if null)</string>
<string name="command_description_upgrade_room">Upgrades a room to a new version</string>
<string name="event_status_a11y_sending">Sending</string>
<string name="event_status_a11y_sent">Sent</string>
@ -3410,5 +3416,21 @@
<string name="teammate_spaces_arent_quite_ready">"Teammate spaces arent quite ready but you can still give them a try"</string>
<string name="teammate_spaces_might_not_join">"At the moment people might not be able to join any private rooms you make.\n\nWell be improving this as part of the beta, but just wanted to let you know."</string>
<string name="joining_replacement_room">Join replacement room</string>
<string name="it_may_take_some_time">Please be patient, it may take some time.</string>
<string name="upgrade">Upgrade</string>
<string name="upgrade_public_room">Upgrade public room</string>
<string name="upgrade_private_room">Upgrade private room</string>
<string name="upgrade_room_warning">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.</string>
<string name="upgrade_public_room_from_to">You\'ll upgrade this room from %s to %s.</string>
<string name="upgrade_room_auto_invite">Automatically invite users</string>
<string name="upgrade_room_update_parent_space">Automatically update space parent</string>
<string name="upgrade_room_no_power_to_manage">You need permission to upgrade a room</string>
<string name="room_using_unstable_room_version">This room is running room version %s, which this homeserver has marked as unstable.</string>
<string name="room_upgrade_to_recommended_version">Upgrade to the recommended room version</string>
<string name="error_failed_to_join_room">Sorry, an error occurred while trying to join: %s</string>
</resources>