Ugrade unstable room notice in settings

default update parent, clean migrate bottomsheet layout
This commit is contained in:
Valere 2021-06-24 09:38:20 +02:00
parent 171793d190
commit 57c75f8039
17 changed files with 239 additions and 192 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021 New Vector Ltd
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -26,7 +26,8 @@ interface RoomVersionService {
*/
suspend fun upgradeToVersion(version: String): String
suspend fun getRecommendedVersion() : String
fun getRecommendedVersion() : String
fun userMayUpgradeRoom(userId: String): Boolean
fun isUsingUnstableRoomVersion(): Boolean
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021 New Vector Ltd
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021 New Vector Ltd
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021 New Vector Ltd
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -24,6 +24,7 @@ import io.realm.Realm
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.homeserver.RoomVersionStatus
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
@ -63,7 +64,7 @@ internal class DefaultRoomVersionService @AssistedInject constructor(
)
}
override suspend fun getRecommendedVersion(): String {
override fun getRecommendedVersion(): String {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
HomeServerCapabilitiesEntity.get(realm)?.let {
HomeServerCapabilitiesMapper.map(it)
@ -71,6 +72,18 @@ internal class DefaultRoomVersionService @AssistedInject constructor(
}
}
override fun isUsingUnstableRoomVersion(): Boolean {
var isUsingUnstable: Boolean
Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val versionCaps = HomeServerCapabilitiesEntity.get(realm)?.let {
HomeServerCapabilitiesMapper.map(it)
}?.roomVersions
val currentVersion = getRoomVersion()
isUsingUnstable = versionCaps?.supportedVersion?.firstOrNull { it.version == currentVersion }?.status == RoomVersionStatus.UNSTABLE
}
return isUsingUnstable
}
override fun userMayUpgradeRoom(userId: String): Boolean {
val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
?.content?.toModel<PowerLevelsContent>()

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021 New Vector Ltd
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

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

@ -332,7 +332,11 @@ object CommandParser {
}
Command.UPGRADE_ROOM.command -> {
val newVersion = textMessage.substring(Command.UPGRADE_ROOM.command.length).trim()
ParsedCommand.UpgradeRoom(newVersion)
if (newVersion.isEmpty()) {
ParsedCommand.ErrorSyntax(Command.UPGRADE_ROOM)
} else {
ParsedCommand.UpgradeRoom(newVersion)
}
}
else -> {
// Unknown command

View file

@ -21,22 +21,24 @@ import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.setFragmentResult
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.databinding.BottomSheetGenericListBinding
import im.vector.app.databinding.BottomSheetRoomUpgradeBinding
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
class MigrateRoomBottomSheet :
VectorBaseBottomSheetDialogFragment<BottomSheetGenericListBinding>(),
MigrateRoomViewModel.Factory, MigrateRoomController.InteractionListener {
VectorBaseBottomSheetDialogFragment<BottomSheetRoomUpgradeBinding>(),
MigrateRoomViewModel.Factory {
@Parcelize
data class Args(
@ -50,7 +52,7 @@ class MigrateRoomBottomSheet :
override val showExpanded = true
@Inject
lateinit var epoxyController: MigrateRoomController
lateinit var errorFormatter: ErrorFormatter
val viewModel: MigrateRoomViewModel by fragmentViewModel()
@ -58,41 +60,79 @@ class MigrateRoomBottomSheet :
injector.inject(this)
}
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
BottomSheetGenericListBinding.inflate(inflater, container, false)
override fun invalidate() = withState(viewModel) { state ->
epoxyController.setData(state)
super.invalidate()
when (val result = state.upgradingStatus) {
views.headerText.setText(if (state.isPublic) R.string.upgrade_public_room else R.string.upgrade_private_room)
views.upgradeFromTo.text = getString(R.string.upgrade_public_room_from_to, state.currentVersion, state.newVersion)
views.autoInviteSwitch.isVisible = !state.isPublic && state.otherMemberCount > 0
views.autoUpdateParent.isVisible = state.knownParents.isNotEmpty()
when (state.upgradingStatus) {
is Loading -> {
views.progressBar.isVisible = true
views.progressBar.isIndeterminate = state.upgradingProgressIndeterminate
views.progressBar.progress = state.upgradingProgress
views.progressBar.max = state.upgradingProgressTotal
views.inlineError.setTextOrHide(null)
views.button.isVisible = false
}
is Success -> {
val result = result.invoke()
if (result is UpgradeRoomViewModelTask.Result.Success) {
setFragmentResult(REQUEST_KEY, Bundle().apply {
putString(BUNDLE_KEY_REPLACEMENT_ROOM, result.replacementRoomId)
})
dismiss()
views.progressBar.isVisible = false
when (val result = state.upgradingStatus.invoke()) {
is UpgradeRoomViewModelTask.Result.Failure -> {
val errorText = when (result) {
is UpgradeRoomViewModelTask.Result.UnknownRoom -> {
// should not happen
getString(R.string.unknown_error)
}
is UpgradeRoomViewModelTask.Result.NotAllowed -> {
getString(R.string.upgrade_room_no_power_to_manage)
}
is UpgradeRoomViewModelTask.Result.ErrorFailure -> {
errorFormatter.toHumanReadable(result.throwable)
}
else -> null
}
views.inlineError.setTextOrHide(errorText)
views.button.isVisible = true
views.button.text = getString(R.string.global_retry)
}
is UpgradeRoomViewModelTask.Result.Success -> {
setFragmentResult(REQUEST_KEY, Bundle().apply {
putString(BUNDLE_KEY_REPLACEMENT_ROOM, result.replacementRoomId)
})
dismiss()
}
}
}
else -> {
views.button.isVisible = true
views.button.text = getString(R.string.upgrade)
}
}
super.invalidate()
}
val postBuild = OnModelBuildFinishedListener {
view?.post { forceExpandState() }
}
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
BottomSheetRoomUpgradeBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
epoxyController.callback = this
views.bottomSheetRecyclerView.configureWith(epoxyController)
epoxyController.addModelBuildListener(postBuild)
}
override fun onDestroyView() {
views.bottomSheetRecyclerView.cleanup()
epoxyController.removeModelBuildListener(postBuild)
super.onDestroyView()
views.button.debouncedClicks {
viewModel.handle(MigrateRoomAction.UpgradeRoom)
}
views.autoInviteSwitch.setOnCheckedChangeListener { _, isChecked ->
viewModel.handle(MigrateRoomAction.SetAutoInvite(isChecked))
}
views.autoUpdateParent.setOnCheckedChangeListener { _, isChecked ->
viewModel.handle(MigrateRoomAction.SetUpdateKnownParentSpace(isChecked))
}
}
override fun create(initialState: MigrateRoomViewState): MigrateRoomViewModel {
@ -111,16 +151,4 @@ class MigrateRoomBottomSheet :
}
}
}
override fun onAutoInvite(autoInvite: Boolean) {
viewModel.handle(MigrateRoomAction.SetAutoInvite(autoInvite))
}
override fun onAutoUpdateParent(update: Boolean) {
viewModel.handle(MigrateRoomAction.SetUpdateKnownParentSpace(update))
}
override fun onConfirmUpgrade() {
viewModel.handle(MigrateRoomAction.UpgradeRoom)
}
}

View file

@ -1,135 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.upgrade
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import im.vector.app.R
import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.bottomsheet.bottomSheetTitleItem
import im.vector.app.core.ui.list.ItemStyle
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.genericProgressBarItem
import im.vector.app.features.form.formSubmitButtonItem
import im.vector.app.features.form.formSwitchItem
import javax.inject.Inject
class MigrateRoomController @Inject constructor(
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter
) : TypedEpoxyController<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

@ -26,7 +26,7 @@ data class MigrateRoomViewState(
val currentVersion: String? = null,
val isPublic: Boolean = false,
val shouldIssueInvites: Boolean = false,
val shouldUpdateKnownParents: Boolean = false,
val shouldUpdateKnownParents: Boolean = true,
val otherMemberCount: Int = 0,
val knownParents: List<String> = emptyList(),
val upgradingStatus: Async<UpgradeRoomViewModelTask.Result> = Uninitialized,

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_recommened_version))
buttonClickAction { host.callback?.doMigrateToVersion(data.recommendedRoomVersion) }
}
}
val learnMoreSubtitle = if (roomSummary.isEncrypted) {
if (roomSummary.isDirect) R.string.direct_room_profile_encrypted_subtitle else R.string.room_profile_encrypted_subtitle
} else {
@ -194,7 +220,7 @@ class RoomProfileController @Inject constructor(
editable = false,
action = { callback?.onRoomIdClicked() }
)
data.roomCreateContent()?.roomVersion?.let {
roomVersion?.let {
buildProfileAction(
id = "roomVersion",
title = stringProvider.getString(R.string.room_settings_room_version_title),

View file

@ -45,6 +45,7 @@ import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.databinding.FragmentMatrixProfileBinding
import im.vector.app.databinding.ViewStubRoomProfileHeaderBinding
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
import im.vector.app.features.home.room.list.actions.RoomListActionsArgs
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
@ -303,6 +304,11 @@ class RoomProfileFragment @Inject constructor(
copyToClipboard(requireContext(), url, true)
}
override fun doMigrateToVersion(newVersion: String) {
MigrateRoomBottomSheet.newInstance(roomProfileArgs.roomId, newVersion)
.show(parentFragmentManager, "migrate")
}
private fun onShareRoomProfile(permalink: String) {
startSharePlainTextIntent(
fragment = this,

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

@ -6,6 +6,7 @@
android:layout_height="match_parent"
android:background="?colorSurface"
android:fadeScrollbars="false"
android:nestedScrollingEnabled="false"
android:scrollbars="vertical"
tools:itemCount="5"
tools:listitem="@layout/item_bottom_sheet_action" />

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="?colorSurface"
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:textColor="?vctr_content_secondary"
android:text="@string/upgrade_room_warning" />
<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"
tools:checked="true"
android:layout_marginBottom="8dp"
android:text="@string/upgrade_room_auto_invite"
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_sapce"
tools:visibility="visible" />
<Button
android:id="@+id/button"
style="@style/Widget.Vector.Button.Outlined"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/upgrade"
android:textAllCaps="true"
android:textColor="?colorSecondary" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
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

@ -3427,4 +3427,7 @@
<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>
<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_recommened_version">Upgrade to the recommended room version</string>
</resources>