room version cap support + room upgrade

This commit is contained in:
Valere 2021-06-22 17:35:39 +02:00
parent 0c88d11429
commit 171793d190
45 changed files with 1257 additions and 46 deletions

View File

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

View File

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

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

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,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)
}
)
}
}
}
)
}
}

View File

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

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,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()

View File

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

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

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

View File

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

View File

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

View File

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

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

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

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,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<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,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"

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,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)

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,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<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) {
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)
}
}
}
}
}

View File

@ -108,4 +108,5 @@ sealed class RoomDetailAction : VectorViewModelAction {
// Failed messages
object RemoveAllFailedMessages : RoomDetailAction()
data class RoomUpgradeSuccess(val replacementRoom: String): RoomDetailAction()
}

View File

@ -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<String>) {
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
}
}
}

View File

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

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

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,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<BottomSheetGenericListBinding>(),
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)
}
}

View File

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

View File

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

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 = false,
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

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

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="?colorSurface"
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

@ -0,0 +1,6 @@
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/genericProgressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp" />

View File

@ -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…" />
</FrameLayout>

View File

@ -1,38 +1,34 @@
<?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:paddingStart="4dp"
android:paddingEnd="0dp"
android:accessibilityLiveRegion="polite"
android:gravity="center"
android:gravity="start"
android:textColor="?vctr_content_primary"
tools:text="@string/room_do_not_have_permission_to_post" />
</merge>
</LinearLayout>

View File

@ -926,7 +926,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>
@ -1820,7 +1820,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>
@ -2735,6 +2735,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>
<!-- Only visible in developer mode-->
<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>
@ -3295,6 +3300,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>
@ -3406,4 +3412,19 @@
<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>
</resources>
<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_sapce">Automatically update space parent</string>
<string name="upgrade_room_update_parent">Automatically update parent space</string>
<string name="upgrade_room_no_power_to_manage">You need permission to upgrade a room</string>
</resources>