From 69350ef514400a28deb0bc9ceeaf2899ad312c62 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 17 Jun 2021 16:17:38 +0300 Subject: [PATCH 001/339] Voice message UI initial implementation. --- build.gradle | 3 + vector/build.gradle | 3 + .../home/room/detail/RoomDetailAction.kt | 4 + .../home/room/detail/RoomDetailFragment.kt | 13 +++ .../home/room/detail/RoomDetailViewModel.kt | 23 +++++ .../room/detail/composer/TextComposerView.kt | 39 +++++++- .../composer/VoiceMessageRecorderView.kt | 92 +++++++++++++++++++ .../composer/VoiceMessageRecordingHelper.kt | 87 ++++++++++++++++++ .../res/drawable/bg_voice_message_lock.xml | 14 +++ .../main/res/drawable/ic_voice_lock_arrow.xml | 13 +++ .../src/main/res/drawable/ic_voice_locked.xml | 5 + .../drawable/ic_voice_message_unlocked.xml | 5 + vector/src/main/res/drawable/ic_voice_mic.xml | 12 +++ .../res/drawable/ic_voice_mic_recording.xml | 18 ++++ .../ic_voice_slide_to_cancel_arrow.xml | 8 ++ .../src/main/res/layout/composer_layout.xml | 6 ++ ...composer_layout_constraint_set_compact.xml | 8 ++ .../layout/view_voice_message_recorder.xml | 69 ++++++++++++++ vector/src/main/res/values/colors.xml | 5 + vector/src/main/res/values/strings.xml | 4 + 20 files changed, 428 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt create mode 100644 vector/src/main/res/drawable/bg_voice_message_lock.xml create mode 100644 vector/src/main/res/drawable/ic_voice_lock_arrow.xml create mode 100644 vector/src/main/res/drawable/ic_voice_locked.xml create mode 100644 vector/src/main/res/drawable/ic_voice_message_unlocked.xml create mode 100644 vector/src/main/res/drawable/ic_voice_mic.xml create mode 100644 vector/src/main/res/drawable/ic_voice_mic_recording.xml create mode 100644 vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml create mode 100644 vector/src/main/res/layout/view_voice_message_recorder.xml diff --git a/build.gradle b/build.gradle index 881cd340f1..a7acc1c124 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,9 @@ allprojects { // Chat effects includeGroupByRegex 'com\\.github\\.jetradarmobile' includeGroupByRegex 'nl\\.dionsegijn' + + // Voice RecordView + includeGroupByRegex 'com\\.github\\.3llomi' } } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } diff --git a/vector/build.gradle b/vector/build.gradle index a94f796a90..991e483e98 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -144,6 +144,8 @@ android { buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" + buildConfigField "Long", "VOICE_MESSAGE_DURATION_LIMIT_MS", "120000L" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // Keep abiFilter for the universalApk @@ -391,6 +393,7 @@ dependencies { implementation "androidx.autofill:autofill:$autofill_version" implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' + implementation 'com.github.3llomi:RecordView:3.0.1' // Custom Tab implementation 'androidx.browser:browser:1.3.0' diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 72e614c18c..e63fc9e17b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -109,4 +109,8 @@ sealed class RoomDetailAction : VectorViewModelAction { // Failed messages object RemoveAllFailedMessages : RoomDetailAction() + + // Voice Message + object StartRecordingVoiceMessage : RoomDetailAction() + data class EndRecordingVoiceMessage(val recordTime: Long) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 9ed4feebc4..024a37067a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -100,6 +100,7 @@ import im.vector.app.core.ui.views.NotificationAreaView import im.vector.app.core.utils.Debouncer import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.KeyboardStateUtils +import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.colorizeMatchingText @@ -1192,6 +1193,18 @@ class RoomDetailFragment @Inject constructor( override fun onTextEmptyStateChanged(isEmpty: Boolean) { // No op } + + override fun onVoiceRecordingStarted() { + roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage) + } + + override fun onVoiceRecordingEnded(recordTime: Long) { + roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(recordTime)) + } + + override fun checkVoiceRecordingPermission(): Boolean { + return checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, requireActivity(), 0) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index a2041c0a80..55ee63091e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail import android.net.Uri import androidx.annotation.IdRes +import androidx.core.net.toUri import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail @@ -38,6 +39,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.lookup.CallProtocolsChecker @@ -47,6 +49,7 @@ import im.vector.app.features.command.ParsedCommand import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider +import im.vector.app.features.home.room.detail.composer.VoiceMessageRecordingHelper import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder @@ -56,6 +59,7 @@ import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.multipicker.utils.toMultiPickerAudioType import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers @@ -119,6 +123,7 @@ class RoomDetailViewModel @AssistedInject constructor( private val chatEffectManager: ChatEffectManager, private val directRoomHelper: DirectRoomHelper, private val jitsiService: JitsiService, + private val voiceMessageRecordingHelper: VoiceMessageRecordingHelper, timelineSettingsFactory: TimelineSettingsFactory ) : VectorViewModel(initialState), Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener { @@ -324,6 +329,8 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() RoomDetailAction.ResendAll -> handleResendAll() + RoomDetailAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage() + is RoomDetailAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.recordTime) }.exhaustive } @@ -611,6 +618,22 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleStartRecordingVoiceMessage() { + voiceMessageRecordingHelper.startRecording() + } + + private fun handleEndRecordingVoiceMessage(recordTime: Long) { + if (recordTime == 0L) { + voiceMessageRecordingHelper.deleteRecording() + return + } + voiceMessageRecordingHelper.stopRecording(recordTime)?.let { audioType -> + room.sendMedia(audioType.toContentAttachmentData(), false, emptySet()) + room + //voiceMessageRecordingHelper.deleteRecording() + } + } + private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled() fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state -> diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt index d5e24dbb6b..6672027133 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt @@ -24,6 +24,7 @@ import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.text.toSpannable +import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.transition.ChangeBounds import androidx.transition.Fade @@ -32,6 +33,7 @@ import androidx.transition.TransitionManager import androidx.transition.TransitionSet import im.vector.app.R import im.vector.app.databinding.ComposerLayoutBinding +import org.matrix.android.sdk.api.extensions.orFalse /** * Encapsulate the timeline composer UX. @@ -46,6 +48,9 @@ class TextComposerView @JvmOverloads constructor( fun onCloseRelatedMessage() fun onSendMessage(text: CharSequence) fun onAddAttachment() + fun onVoiceRecordingStarted() + fun onVoiceRecordingEnded(recordTime: Long) + fun checkVoiceRecordingPermission(): Boolean } val views: ComposerLayoutBinding @@ -71,7 +76,9 @@ class TextComposerView @JvmOverloads constructor( } override fun onTextEmptyStateChanged(isEmpty: Boolean) { - views.sendButton.isVisible = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isEmpty + val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isEmpty + views.sendButton.isInvisible = !shouldShowSendButton + views.voiceMessageRecorderView.isVisible = !shouldShowSendButton } } views.composerRelatedMessageCloseButton.setOnClickListener { @@ -87,6 +94,28 @@ class TextComposerView @JvmOverloads constructor( views.attachmentButton.setOnClickListener { callback?.onAddAttachment() } + + views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback { + override fun onVoiceRecordingStarted() { + views.attachmentButton.isVisible = false + views.composerEditText.isVisible = false + views.composerEmojiButton.isVisible = false + views.composerEditTextOuterBorder.isVisible = false + callback?.onVoiceRecordingStarted() + } + + override fun onVoiceRecordingEnded(recordTime: Long) { + views.attachmentButton.isVisible = true + views.composerEditText.isVisible = true + views.composerEmojiButton.isVisible = true + views.composerEditTextOuterBorder.isVisible = true + callback?.onVoiceRecordingEnded(recordTime) + } + + override fun checkVoiceRecordingPermission(): Boolean { + return callback?.checkVoiceRecordingPermission().orFalse() + } + } } fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { @@ -96,7 +125,10 @@ class TextComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_compact applyNewConstraintSet(animate, transitionComplete) - views.sendButton.isVisible = !views.composerEditText.text.isNullOrEmpty() + + val shouldShowSendButton = !views.composerEditText.text.isNullOrEmpty() + views.sendButton.isInvisible = !shouldShowSendButton + views.voiceMessageRecorderView.isVisible = !shouldShowSendButton } fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { @@ -106,7 +138,8 @@ class TextComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded applyNewConstraintSet(animate, transitionComplete) - views.sendButton.isVisible = true + views.sendButton.isInvisible = false + views.voiceMessageRecorderView.isVisible = false } private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt new file mode 100644 index 0000000000..8ae32fcc69 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -0,0 +1,92 @@ +/* + * 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.composer + +import android.content.Context +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import com.devlomi.record_view.OnRecordListener +import im.vector.app.BuildConfig +import im.vector.app.R +import im.vector.app.databinding.ViewVoiceMessageRecorderBinding +import org.matrix.android.sdk.api.extensions.orFalse + +/** + * Encapsulates the voice message recording view and animations. + */ +class VoiceMessageRecorderView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + interface Callback { + fun onVoiceRecordingStarted() + fun onVoiceRecordingEnded(recordTime: Long) + fun checkVoiceRecordingPermission(): Boolean + } + + private val views: ViewVoiceMessageRecorderBinding + + var callback: Callback? = null + + init { + inflate(context, R.layout.view_voice_message_recorder, this) + views = ViewVoiceMessageRecorderBinding.bind(this) + + views.voiceMessageButton.setRecordView(views.voiceMessageRecordView) + views.voiceMessageRecordView.timeLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS + + views.voiceMessageRecordView.setRecordPermissionHandler { callback?.checkVoiceRecordingPermission().orFalse() } + + views.voiceMessageRecordView.setOnRecordListener(object : OnRecordListener { + override fun onStart() { + onVoiceRecordingStarted() + } + + override fun onCancel() { + onVoiceRecordingEnded(0) + } + + override fun onFinish(recordTime: Long, limitReached: Boolean) { + onVoiceRecordingEnded(recordTime) + } + + override fun onLessThanSecond() { + onVoiceRecordingEnded(0) + } + }) + } + + private fun onVoiceRecordingStarted() { + views.voiceMessageLockBackground.isVisible = true + views.voiceMessageLockArrow.isVisible = true + views.voiceMessageLockImage.isVisible = true + views.voiceMessageButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_voice_mic_recording)) + callback?.onVoiceRecordingStarted() + } + + private fun onVoiceRecordingEnded(recordTime: Long) { + views.voiceMessageLockBackground.isVisible = false + views.voiceMessageLockArrow.isVisible = false + views.voiceMessageLockImage.isVisible = false + views.voiceMessageButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_voice_mic)) + callback?.onVoiceRecordingEnded(recordTime) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt new file mode 100644 index 0000000000..63cbbe6e79 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.composer + +import android.content.Context +import android.media.MediaRecorder +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import im.vector.app.BuildConfig +import im.vector.lib.multipicker.entity.MultiPickerAudioType +import im.vector.lib.multipicker.utils.toMultiPickerAudioType +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import java.lang.RuntimeException +import java.util.UUID +import javax.inject.Inject + +/** + * Helper class to record audio for voice messages. + */ +class VoiceMessageRecordingHelper @Inject constructor( + private val context: Context +) { + + private lateinit var mediaRecorder: MediaRecorder + private val outputDirectory = File(context.cacheDir, "downloads") + private var outputFile: File? = null + + init { + if (!outputDirectory.exists()) { + outputDirectory.mkdirs() + } + } + + private fun refreshMediaRecorder() { + mediaRecorder = MediaRecorder() + mediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT) + mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.OGG) + mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS) + mediaRecorder.setAudioEncodingBitRate(24000) + mediaRecorder.setAudioSamplingRate(48000) + } + + fun startRecording() { + outputFile = File(outputDirectory, UUID.randomUUID().toString() + ".ogg") + FileOutputStream(outputFile).use { fos -> + refreshMediaRecorder() + mediaRecorder.setOutputFile(fos.fd) + mediaRecorder.prepare() + mediaRecorder.start() + } + } + + fun stopRecording(recordTime: Long): MultiPickerAudioType? { + try { + mediaRecorder.stop() + mediaRecorder.reset() + mediaRecorder.release() + outputFile?.let { + val outputFileUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", it) + return outputFileUri?.toMultiPickerAudioType(context) + } ?: return null + } catch (e: RuntimeException) { // Usually thrown when the record is less than 1 second. + Timber.e(e, "Voice message is not valid. Record time: %s", recordTime) + return null + } + } + + fun deleteRecording() { + outputFile?.delete() + } +} diff --git a/vector/src/main/res/drawable/bg_voice_message_lock.xml b/vector/src/main/res/drawable/bg_voice_message_lock.xml new file mode 100644 index 0000000000..672d7bf80f --- /dev/null +++ b/vector/src/main/res/drawable/bg_voice_message_lock.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_voice_lock_arrow.xml b/vector/src/main/res/drawable/ic_voice_lock_arrow.xml new file mode 100644 index 0000000000..7f9f2403ad --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_lock_arrow.xml @@ -0,0 +1,13 @@ + + + diff --git a/vector/src/main/res/drawable/ic_voice_locked.xml b/vector/src/main/res/drawable/ic_voice_locked.xml new file mode 100644 index 0000000000..2b92d9d5e0 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_locked.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/drawable/ic_voice_message_unlocked.xml b/vector/src/main/res/drawable/ic_voice_message_unlocked.xml new file mode 100644 index 0000000000..007de349ec --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_message_unlocked.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/drawable/ic_voice_mic.xml b/vector/src/main/res/drawable/ic_voice_mic.xml new file mode 100644 index 0000000000..7cb091afaa --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_mic.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_voice_mic_recording.xml b/vector/src/main/res/drawable/ic_voice_mic_recording.xml new file mode 100644 index 0000000000..eb6cea39a5 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_mic_recording.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml b/vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml new file mode 100644 index 0000000000..1299e82530 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml @@ -0,0 +1,8 @@ + + + diff --git a/vector/src/main/res/layout/composer_layout.xml b/vector/src/main/res/layout/composer_layout.xml index 3816c206b4..7c9c23645d 100644 --- a/vector/src/main/res/layout/composer_layout.xml +++ b/vector/src/main/res/layout/composer_layout.xml @@ -131,4 +131,10 @@ android:src="@drawable/ic_send" tools:ignore="MissingConstraints" /> + + diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml index 079bc9705a..b51f69302a 100644 --- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml +++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml @@ -178,4 +178,12 @@ tools:ignore="MissingPrefix" tools:visibility="visible" /> + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml new file mode 100644 index 0000000000..3b124ae7ef --- /dev/null +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/colors.xml b/vector/src/main/res/values/colors.xml index 7158554833..f66476a795 100644 --- a/vector/src/main/res/values/colors.xml +++ b/vector/src/main/res/values/colors.xml @@ -132,4 +132,9 @@ @color/black_alpha @android:color/transparent + + #FFF3F8FD + #22252B + #22252B + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 80120b51bf..b2519f60b2 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3399,4 +3399,8 @@ Some rooms may be hidden because they’re private and you need an invite. Unnamed Room + + Start Voice Message + Slide to cancel + Voice Message Lock From cb96886568e1969fdbe43f069a08ad792951ef32 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 17 Jun 2021 16:18:20 +0300 Subject: [PATCH 002/339] Send voice message. --- .../java/org/matrix/android/sdk/rx/RxRoom.kt | 5 ++++ .../room/model/message/MessageAudioContent.kt | 13 +++++++- .../crypto/model/rest/AudioWaveformInfo.kt | 30 +++++++++++++++++++ .../room/send/LocalEchoEventFactory.kt | 9 +++++- .../multipicker/utils/ContentResolverUtil.kt | 2 +- 5 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/AudioWaveformInfo.kt diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt index 21db4e1893..b3495c4493 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt @@ -23,6 +23,7 @@ import io.reactivex.Single import kotlinx.coroutines.rx2.rxCompletable import kotlinx.coroutines.rx2.rxSingle import org.matrix.android.sdk.api.query.QueryStringValue +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.identity.ThreePid import org.matrix.android.sdk.api.session.room.Room @@ -146,6 +147,10 @@ class RxRoom(private val room: Room) { fun deleteAvatar(): Completable = rxCompletable { room.deleteAvatar() } + + fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set): Completable = rxCompletable { + room.sendMedia(attachment, compressBeforeSending, roomIds) + } } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt index fcf3d73cbe..3ce4cd6932 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt @@ -20,6 +20,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.AudioWaveformInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) @@ -50,7 +51,17 @@ data class MessageAudioContent( /** * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ - @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null, + + /** + * Encapsulates waveform and duration of the audio. + */ + @Json(name = "org.matrix.msc1767.audio") val audioWaveformInfo: AudioWaveformInfo? = null, + + /** + * Indicates that is a voice message. + */ + @Json(name = "org.matrix.msc2516.voice") val voiceMessageIndicator: Any? = null ) : MessageWithAttachmentContent { override val mimeType: String? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/AudioWaveformInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/AudioWaveformInfo.kt new file mode 100644 index 0000000000..31a356e164 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/AudioWaveformInfo.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AudioWaveformInfo( + @Json(name = "duration") + val duration: Long? = null, + + @Json(name = "waveform") + val waveform: List? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index f505b13b33..03afa4beb6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -53,6 +53,7 @@ import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.isReply +import org.matrix.android.sdk.internal.crypto.model.rest.AudioWaveformInfo import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory @@ -289,6 +290,7 @@ internal class LocalEchoEventFactory @Inject constructor( } private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event { + val isVoiceMessage = attachment.mimeType == "audio/ogg" val content = MessageAudioContent( msgType = MessageType.MSGTYPE_AUDIO, body = attachment.name ?: "audio", @@ -296,7 +298,12 @@ internal class LocalEchoEventFactory @Inject constructor( mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }, size = attachment.size ), - url = attachment.queryUri.toString() + url = attachment.queryUri.toString(), + audioWaveformInfo = if (!isVoiceMessage) null else AudioWaveformInfo( + duration = attachment.duration, + waveform = null // TODO. + ), + voiceMessageIndicator = if (!isVoiceMessage) null else Any() ) return createMessageEvent(roomId, content) } diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt b/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt index a1982b0bbc..6cadfa6ce7 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt @@ -111,7 +111,7 @@ internal fun Uri.toMultiPickerVideoType(context: Context): MultiPickerVideoType? } } -internal fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? { +fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? { val projection = arrayOf( MediaStore.Audio.Media.DISPLAY_NAME, MediaStore.Audio.Media.SIZE From 171793d19017275b3daf64d5152d8254903604b2 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 22 Jun 2021 17:35:39 +0200 Subject: [PATCH 003/339] room version cap support + room upgrade --- .../homeserver/HomeServerCapabilities.kt | 6 +- .../session/homeserver/RoomVersionModel.kt | 32 +++++ .../android/sdk/api/session/room/Room.kt | 4 +- .../room/version/RoomVersionService.kt | 32 +++++ .../android/sdk/api/session/space/Space.kt | 3 + .../database/RealmSessionStoreMigration.kt | 9 +- .../mapper/HomeServerCapabilitiesMapper.kt | 23 ++- .../model/HomeServerCapabilitiesEntity.kt | 3 +- .../homeserver/GetCapabilitiesResult.kt | 22 ++- .../GetHomeServerCapabilitiesTask.kt | 5 + .../sdk/internal/session/room/DefaultRoom.kt | 7 +- .../sdk/internal/session/room/RoomAPI.kt | 11 ++ .../sdk/internal/session/room/RoomFactory.kt | 5 +- .../sdk/internal/session/room/RoomModule.kt | 5 + .../internal/session/room/RoomUpgradeBody.kt | 26 ++++ .../session/room/RoomUpgradeResponse.kt | 26 ++++ .../room/summary/RoomSummaryUpdater.kt | 2 +- .../room/version/DefaultRoomVersionService.kt | 85 +++++++++++ .../room/version/RoomVersionUpgradeTask.kt | 66 +++++++++ .../internal/session/space/DefaultSpace.kt | 6 + .../java/im/vector/app/VectorApplication.kt | 1 - .../im/vector/app/core/di/ScreenComponent.kt | 4 + .../VectorBaseBottomSheetDialogFragment.kt | 6 +- .../core/ui/list/GenericProgressBarItem.kt | 53 +++++++ .../app/core/ui/views/NotificationAreaView.kt | 6 +- .../im/vector/app/features/command/Command.kt | 3 +- .../app/features/command/CommandParser.kt | 10 +- .../app/features/command/ParsedCommand.kt | 1 + .../detail/JoinReplacementRoomBottomSheet.kt | 87 +++++++++++ .../home/room/detail/RoomDetailAction.kt | 1 + .../home/room/detail/RoomDetailFragment.kt | 32 ++++- .../home/room/detail/RoomDetailViewEvents.kt | 2 + .../home/room/detail/RoomDetailViewModel.kt | 27 ++++ .../room/detail/upgrade/MigrateRoomAction.kt | 25 ++++ .../detail/upgrade/MigrateRoomBottomSheet.kt | 126 ++++++++++++++++ .../detail/upgrade/MigrateRoomController.kt | 135 ++++++++++++++++++ .../detail/upgrade/MigrateRoomViewModel.kt | 116 +++++++++++++++ .../detail/upgrade/MigrateRoomViewState.kt | 41 ++++++ .../upgrade/UpgradeRoomViewModelTask.kt | 99 +++++++++++++ .../HomeserverSettingsController.kt | 37 ++++- .../layout/bottom_sheet_tombstone_join.xml | 48 +++++++ .../main/res/layout/item_generic_progress.xml | 6 + .../res/layout/item_timeline_event_create.xml | 4 +- .../res/layout/view_notification_area.xml | 28 ++-- vector/src/main/res/values/strings.xml | 27 +++- 45 files changed, 1257 insertions(+), 46 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt create mode 100644 vector/src/main/java/im/vector/app/core/ui/list/GenericProgressBarItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt create mode 100644 vector/src/main/res/layout/bottom_sheet_tombstone_join.xml create mode 100644 vector/src/main/res/layout/item_generic_progress.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index da99ab8d54..fc3d09d91d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.session.homeserver +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities.Companion.MAX_UPLOAD_FILE_SIZE_UNKNOWN + data class HomeServerCapabilities( /** * True if it is possible to change the password of the account. @@ -32,7 +34,9 @@ data class HomeServerCapabilities( /** * Default identity server url, provided in Wellknown */ - val defaultIdentityServerUrl: String? = null + val defaultIdentityServerUrl: String? = null, + + val roomVersions: RoomVersionCapabilities? = null ) { companion object { const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt new file mode 100644 index 0000000000..5997df035e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.homeserver + +data class RoomVersionCapabilities( + val defaultRoomVersion: String, + val supportedVersion: List +) + +data class RoomVersionInfo( + val version: String, + val status: RoomVersionStatus +) + +enum class RoomVersionStatus { + STABLE, + UNSTABLE +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index cb04b05a74..ebe96b6382 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.tags.TagsService import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService +import org.matrix.android.sdk.api.session.room.version.RoomVersionService import org.matrix.android.sdk.api.session.search.SearchResult import org.matrix.android.sdk.api.session.space.Space import org.matrix.android.sdk.api.util.Optional @@ -57,7 +58,8 @@ interface Room : RelationService, RoomCryptoService, RoomPushRuleService, - RoomAccountDataService { + RoomAccountDataService, + RoomVersionService { /** * The roomId of this room diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt new file mode 100644 index 0000000000..a90e1343f2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.version + +interface RoomVersionService { + + fun getRoomVersion(): String + + /** + * Upgrade to the given room version + * @return the replacement room id + */ + suspend fun upgradeToVersion(version: String): String + + suspend fun getRecommendedVersion() : String + + fun userMayUpgradeRoom(userId: String): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt index db25762c2f..3bae6126e0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.space import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.space.model.SpaceChildContent interface Space { @@ -38,6 +39,8 @@ interface Space { autoJoin: Boolean = false, suggested: Boolean? = false) + fun getChildInfo(roomId: String): SpaceChildContent? + suspend fun removeChildren(roomId: String) @Throws diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 2b3c3b28ee..864aa13cc2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -46,7 +46,7 @@ import javax.inject.Inject class RealmSessionStoreMigration @Inject constructor() : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 14L + const val SESSION_STORE_SCHEMA_VERSION = 15L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -66,6 +66,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { if (oldVersion <= 11) migrateTo12(realm) if (oldVersion <= 12) migrateTo13(realm) if (oldVersion <= 13) migrateTo14(realm) + if (oldVersion <= 14) migrateTo15(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -306,4 +307,10 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { roomAccountDataSchema.isEmbedded = true } + + private fun migrateTo15(realm: DynamicRealm) { + Timber.d("Step 14 -> 15") + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.ROOM_VERSION_JSON, String::class.java) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index b18c67294f..566df97105 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -16,8 +16,15 @@ package org.matrix.android.sdk.internal.database.mapper +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.homeserver.RoomVersionCapabilities +import org.matrix.android.sdk.api.session.homeserver.RoomVersionInfo +import org.matrix.android.sdk.api.session.homeserver.RoomVersionStatus import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.session.homeserver.RoomVersions +import org.matrix.android.sdk.internal.session.room.version.DefaultRoomVersionService /** * HomeServerCapabilitiesEntity -> HomeSeverCapabilities @@ -29,7 +36,21 @@ internal object HomeServerCapabilitiesMapper { canChangePassword = entity.canChangePassword, maxUploadFileSize = entity.maxUploadFileSize, lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported, - defaultIdentityServerUrl = entity.defaultIdentityServerUrl + defaultIdentityServerUrl = entity.defaultIdentityServerUrl, + roomVersions = entity.roomVersionJson?.let { + tryOrNull { + MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).fromJson(it)?.let { + RoomVersionCapabilities( + defaultRoomVersion = it.default ?: DefaultRoomVersionService.DEFAULT_ROOM_VERSION, + supportedVersion = it.available.entries.map { entry -> + RoomVersionInfo(entry.key, RoomVersionStatus.STABLE + .takeIf { entry.value == "stable" } + ?: RoomVersionStatus.UNSTABLE) + } + ) + } + } + } ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index 763dcf80a2..0af2fc4cc5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -24,7 +24,8 @@ internal open class HomeServerCapabilitiesEntity( var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN, var lastVersionIdentityServerSupported: Boolean = false, var defaultIdentityServerUrl: String? = null, - var lastUpdatedTimestamp: Long = 0L + var lastUpdatedTimestamp: Long = 0L, + var roomVersionJson: String? = null ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt index ab029a0fce..014b1b17da 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.homeserver import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.extensions.orTrue +import org.matrix.android.sdk.api.util.JsonDict /** * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-capabilities @@ -38,9 +39,14 @@ internal data class Capabilities( * Capability to indicate if the user can change their password. */ @Json(name = "m.change_password") - val changePassword: ChangePassword? = null + val changePassword: ChangePassword? = null, - // No need for m.room_versions for the moment + /** + * This capability describes the default and available room versions a server supports, and at what level of stability. + * Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms. + */ + @Json(name = "m.room_versions") + val roomVersions: RoomVersions? = null ) @JsonClass(generateAdapter = true) @@ -52,6 +58,18 @@ internal data class ChangePassword( val enabled: Boolean? ) +@JsonClass(generateAdapter = true) +internal data class RoomVersions( + /** + * Required. True if the user can change their password, false otherwise. + */ + @Json(name = "default") + val default: String?, + + @Json(name = "available") + val available: JsonDict +) + // The spec says: If not present, the client should assume that password changes are possible via the API internal fun GetCapabilitiesResult.canChangePassword(): Boolean { return capabilities?.changePassword?.enabled.orTrue() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index 740370123f..82eb03afe6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver @@ -104,6 +105,10 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( if (getCapabilitiesResult != null) { homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword() + + homeServerCapabilitiesEntity.roomVersionJson = getCapabilitiesResult.capabilities?.roomVersions?.let { + MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it) + } } if (getMediaConfigResult != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 0d9c106d41..1a95996024 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -37,6 +37,7 @@ import org.matrix.android.sdk.api.session.room.tags.TagsService import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService +import org.matrix.android.sdk.api.session.room.version.RoomVersionService import org.matrix.android.sdk.api.session.search.SearchResult import org.matrix.android.sdk.api.session.space.Space import org.matrix.android.sdk.api.util.Optional @@ -69,7 +70,8 @@ internal class DefaultRoom(override val roomId: String, private val roomAccountDataService: RoomAccountDataService, private val sendStateTask: SendStateTask, private val viaParameterFinder: ViaParameterFinder, - private val searchTask: SearchTask) : + private val searchTask: SearchTask, + private val roomVersionService: RoomVersionService) : Room, TimelineService by timelineService, SendService by sendService, @@ -85,7 +87,8 @@ internal class DefaultRoom(override val roomId: String, RelationService by relationService, MembershipService by roomMembersService, RoomPushRuleService by roomPushRuleService, - RoomAccountDataService by roomAccountDataService { + RoomAccountDataService by roomAccountDataService, + RoomVersionService by roomVersionService { override fun getRoomSummaryLive(): LiveData> { return roomSummaryDataSource.getRoomSummaryLive(roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 4f12604039..18ece60629 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -369,4 +369,15 @@ internal interface RoomAPI { @Path("roomId") roomId: String, @Path("type") type: String, @Body content: JsonDict) + + /** + * Upgrades the given room to a particular room version. + * Errors: + * 400, The request was invalid. One way this can happen is if the room version requested is not supported by the homeserver + * (M_UNSUPPORTED_ROOM_VERSION) + * 403: The user is not permitted to upgrade the room.(M_FORBIDDEN) + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/upgrade") + suspend fun upgradeRoom(@Path("roomId") roomId: String, + @Body body: RoomUpgradeBody): RoomUpgradeResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index 9ddb8f1177..6b5565fa50 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -37,6 +37,7 @@ import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService +import org.matrix.android.sdk.internal.session.room.version.DefaultRoomVersionService import org.matrix.android.sdk.internal.session.search.SearchTask import javax.inject.Inject @@ -61,6 +62,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: private val relationServiceFactory: DefaultRelationService.Factory, private val membershipServiceFactory: DefaultMembershipService.Factory, private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory, + private val roomVersionServiceFactory: DefaultRoomVersionService.Factory, private val roomAccountDataServiceFactory: DefaultRoomAccountDataService.Factory, private val sendStateTask: SendStateTask, private val viaParameterFinder: ViaParameterFinder, @@ -89,7 +91,8 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: roomAccountDataService = roomAccountDataServiceFactory.create(roomId), sendStateTask = sendStateTask, searchTask = searchTask, - viaParameterFinder = viaParameterFinder + viaParameterFinder = viaParameterFinder, + roomVersionService = roomVersionServiceFactory.create(roomId) ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index d88c195056..c04c899e18 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -92,6 +92,8 @@ import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask import org.matrix.android.sdk.internal.session.room.uploads.DefaultGetUploadsTask import org.matrix.android.sdk.internal.session.room.uploads.GetUploadsTask +import org.matrix.android.sdk.internal.session.room.version.DefaultRoomVersionUpgradeTask +import org.matrix.android.sdk.internal.session.room.version.RoomVersionUpgradeTask import org.matrix.android.sdk.internal.session.space.DefaultSpaceService import retrofit2.Retrofit @@ -243,4 +245,7 @@ internal abstract class RoomModule { @Binds abstract fun bindGetEventTask(task: DefaultGetEventTask): GetEventTask + + @Binds + abstract fun bindRoomVersionUpgradeTask(task: DefaultRoomVersionUpgradeTask): RoomVersionUpgradeTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt new file mode 100644 index 0000000000..feefbe5f66 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RoomUpgradeBody( + @Json(name = "new_version") + val newVersion: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.kt new file mode 100644 index 0000000000..16e95194a3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RoomUpgradeResponse( + @Json(name = "replacement_room") + val replacementRoomId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 7cbcfee713..7ad6d06b1e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -354,7 +354,7 @@ internal class RoomSummaryUpdater @Inject constructor( // we keep real m.child/m.parent relations and add the one for common memberships dmRoom.flattenParentIds += "|${flattenRelated.joinToString("|")}|" } -// Timber.v("## SPACES: flatten of ${dmRoom.otherMemberIds.joinToString(",")} is ${dmRoom.flattenParentIds}") + Timber.v("## SPACES: flatten of ${dmRoom.otherMemberIds.joinToString(",")} is ${dmRoom.flattenParentIds}") } // Maybe a good place to count the number of notifications for spaces? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt new file mode 100644 index 0000000000..1b5d62926e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.version + +import com.zhuinden.monarchy.Monarchy +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.realm.Realm +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.version.RoomVersionService +import org.matrix.android.sdk.internal.database.mapper.HomeServerCapabilitiesMapper +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource + +internal class DefaultRoomVersionService @AssistedInject constructor( + @Assisted private val roomId: String, + @SessionDatabase private val monarchy: Monarchy, + private val stateEventDataSource: StateEventDataSource, + private val roomVersionUpgradeTask: RoomVersionUpgradeTask +) : RoomVersionService { + + @AssistedFactory + interface Factory { + fun create(roomId: String): DefaultRoomVersionService + } + + override fun getRoomVersion(): String { + return stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_CREATE, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + ?.roomVersion + // as per spec -> Defaults to "1" if the key does not exist. + ?: DEFAULT_ROOM_VERSION + } + + override suspend fun upgradeToVersion(version: String): String { + return roomVersionUpgradeTask.execute( + RoomVersionUpgradeTask.Params( + roomId, version + ) + ) + } + + override suspend fun getRecommendedVersion(): String { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + HomeServerCapabilitiesEntity.get(realm)?.let { + HomeServerCapabilitiesMapper.map(it) + }?.roomVersions?.defaultRoomVersion ?: DEFAULT_ROOM_VERSION + } + } + + override fun userMayUpgradeRoom(userId: String): Boolean { + val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + ?.content?.toModel() + ?.let { PowerLevelsHelper(it) } + + return powerLevelsHelper?.isUserAllowedToSend(userId, true, EventType.STATE_ROOM_TOMBSTONE) ?: false + } + + companion object { + const val DEFAULT_ROOM_VERSION = "1" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt new file mode 100644 index 0000000000..444795b5a9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.version + +import io.realm.RealmConfiguration +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.RoomUpgradeBody +import org.matrix.android.sdk.internal.task.Task +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal interface RoomVersionUpgradeTask : Task { + data class Params( + val roomId: String, + val newVersion: String + ) +} + +internal class DefaultRoomVersionUpgradeTask @Inject constructor( + private val roomAPI: RoomAPI, + private val globalErrorReceiver: GlobalErrorReceiver, + @SessionDatabase + private val realmConfiguration: RealmConfiguration +) : RoomVersionUpgradeTask { + + override suspend fun execute(params: RoomVersionUpgradeTask.Params): String { + val replacementRoomId = executeRequest(globalErrorReceiver) { + roomAPI.upgradeRoom( + roomId = params.roomId, + body = RoomUpgradeBody(params.newVersion) + ) + }.replacementRoomId + + // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before) + tryOrNull { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomSummaryEntity::class.java) + .equalTo(RoomSummaryEntityFields.ROOM_ID, replacementRoomId) + .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + } + } + return replacementRoomId + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt index 70c52bf4ae..233eef45f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt @@ -86,6 +86,12 @@ internal class DefaultSpace( ) } + override fun getChildInfo(roomId: String): SpaceChildContent? { + return room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) + .firstOrNull() + ?.content.toModel() + } + override suspend fun setChildrenOrder(roomId: String, order: String?) { val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) .firstOrNull() diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index f3e2f8740e..37a9dc36b4 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -159,7 +159,6 @@ class VectorApplication : // Do not display the name change popup doNotShowDisclaimerDialog(this) } - if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!! activeSessionHolder.setActiveSession(lastAuthenticatedSession) diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index 3c11bfcd13..7784a2bd1c 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -40,12 +40,14 @@ import im.vector.app.features.debug.DebugMenuActivity import im.vector.app.features.devtools.RoomDevToolActivity import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.HomeModule +import im.vector.app.features.home.room.detail.JoinReplacementRoomBottomSheet import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.app.features.home.room.detail.search.SearchActivity import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet +import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.home.room.filtered.FilteredRoomsActivity import im.vector.app.features.home.room.list.RoomListModule @@ -191,6 +193,8 @@ interface ScreenComponent { fun inject(bottomSheet: SpaceSettingsMenuBottomSheet) fun inject(bottomSheet: InviteRoomSpaceChooserBottomSheet) fun inject(bottomSheet: SpaceInviteBottomSheet) + fun inject(bottomSheet: JoinReplacementRoomBottomSheet) + fun inject(bottomSheet: MigrateRoomBottomSheet) /* ========================================================================================== * Others diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt index d6d4d07500..b9b5bc8ca5 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -46,7 +46,7 @@ import java.util.concurrent.TimeUnit /** * Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment) */ -abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment(), MvRxView { +abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment(), MvRxView { private val mvrxViewIdProperty = MvRxViewId() final override val mvrxViewId: String by mvrxViewIdProperty @@ -168,6 +168,10 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShee @CallSuper override fun invalidate() { + forceExpandState() + } + + protected fun forceExpandState() { if (showExpanded) { // Force the bottom sheet to be expanded bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericProgressBarItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericProgressBarItem.kt new file mode 100644 index 0000000000..48a267ec12 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericProgressBarItem.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.core.ui.list + +import android.widget.ProgressBar +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel + +/** + * A generic list item. + * Displays an item with a title, and optional description. + * Can display an accessory on the right, that can be an image or an indeterminate progress. + * If provided with an action, will display a button at the bottom of the list item. + */ +@EpoxyModelClass(layout = R.layout.item_generic_progress) +abstract class GenericProgressBarItem : VectorEpoxyModel() { + + @EpoxyAttribute + var progress: Int = 0 + + @EpoxyAttribute + var total: Int = 100 + + @EpoxyAttribute + var indeterminate: Boolean = false + + override fun bind(holder: Holder) { + super.bind(holder) + holder.progressbar.progress = progress + holder.progressbar.max = total + holder.progressbar.isIndeterminate = indeterminate + } + + class Holder : VectorEpoxyHolder() { + val progressbar by bind(R.id.genericProgressBar) + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt b/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt index ad2a4b8e0c..9e9fe9b8a0 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt @@ -21,7 +21,7 @@ import android.graphics.Color import android.text.method.LinkMovementMethod import android.util.AttributeSet import android.view.View -import android.widget.RelativeLayout +import android.widget.LinearLayout import androidx.core.content.ContextCompat import androidx.core.text.italic import im.vector.app.R @@ -44,7 +44,7 @@ class NotificationAreaView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : RelativeLayout(context, attrs, defStyleAttr) { +) : LinearLayout(context, attrs, defStyleAttr) { var delegate: Delegate? = null private var state: State = State.Initial @@ -127,7 +127,7 @@ class NotificationAreaView @JvmOverloads constructor( private fun renderTombstone(state: State.Tombstone) { visibility = View.VISIBLE - views.roomNotificationIcon.setImageResource(R.drawable.error) + views.roomNotificationIcon.setImageResource(R.drawable.ic_warning_badge) val message = span { +resources.getString(R.string.room_tombstone_versioned_description) +"\n" diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 61d39857cc..3719618d31 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -50,7 +50,8 @@ enum class Command(val command: String, val parameters: String, @StringRes val d CREATE_SPACE("/createspace", " *", R.string.command_description_create_space, true), ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space, true), JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true), - LEAVE_ROOM("/leave", "", R.string.command_description_leave_room, true); + LEAVE_ROOM("/leave", "", R.string.command_description_leave_room, true), + UPGRADE_ROOM("/upgraderoom", "newVersion", R.string.command_description_upgrade_room, true); val length get() = command.length + 1 diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 3de00f4d0c..224956049c 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -312,24 +312,28 @@ object CommandParser { ) } } - Command.ADD_TO_SPACE.command -> { + Command.ADD_TO_SPACE.command -> { val rawCommand = textMessage.substring(Command.ADD_TO_SPACE.command.length).trim() ParsedCommand.AddToSpace( rawCommand ) } - Command.JOIN_SPACE.command -> { + Command.JOIN_SPACE.command -> { val spaceIdOrAlias = textMessage.substring(Command.JOIN_SPACE.command.length).trim() ParsedCommand.JoinSpace( spaceIdOrAlias ) } - Command.LEAVE_ROOM.command -> { + Command.LEAVE_ROOM.command -> { val spaceIdOrAlias = textMessage.substring(Command.LEAVE_ROOM.command.length).trim() ParsedCommand.LeaveRoom( spaceIdOrAlias ) } + Command.UPGRADE_ROOM.command -> { + val newVersion = textMessage.substring(Command.UPGRADE_ROOM.command.length).trim() + ParsedCommand.UpgradeRoom(newVersion) + } else -> { // Unknown command ParsedCommand.ErrorUnknownSlashCommand(slashCommand) diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index d67caac60a..123f1d3a36 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -61,4 +61,5 @@ sealed class ParsedCommand { class AddToSpace(val spaceId: String) : ParsedCommand() class JoinSpace(val spaceIdOrAlias: String) : ParsedCommand() class LeaveRoom(val roomId: String) : ParsedCommand() + class UpgradeRoom(val newVersion: String) : ParsedCommand() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt new file mode 100644 index 0000000000..972162645d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.platform.ButtonStateView +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetTombstoneJoinBinding +import javax.inject.Inject + +class JoinReplacementRoomBottomSheet : + VectorBaseBottomSheetDialogFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + BottomSheetTombstoneJoinBinding.inflate(inflater, container, false) + + @Inject + lateinit var errorFormatter: ErrorFormatter + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + private val viewModel: RoomDetailViewModel by parentFragmentViewModel() + + override val showExpanded: Boolean + get() = true + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.roomUpgradeButton.retryClicked = object : ClickListener { + override fun invoke(view: View) { + withState(viewModel) { it.tombstoneEvent }?.let { + viewModel.handle(RoomDetailAction.HandleTombstoneEvent(it)) + } + } + } + + viewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling) { joinState -> + when (joinState) { + // it should never be Uninitialized + Uninitialized -> views.roomUpgradeButton.render(ButtonStateView.State.Loaded) + is Loading -> { + views.roomUpgradeButton.render(ButtonStateView.State.Loading) + views.descriptionText.setText(R.string.it_may_take_some_time) + } + is Success -> { + views.roomUpgradeButton.render(ButtonStateView.State.Loaded) + dismiss() + } + is Fail -> { + // display the error message + views.descriptionText.text = errorFormatter.toHumanReadable(joinState.error) + views.roomUpgradeButton.render(ButtonStateView.State.Error) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index c0e73823e4..0e090256d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -108,4 +108,5 @@ sealed class RoomDetailAction : VectorViewModelAction { // Failed messages object RemoveAllFailedMessages : RoomDetailAction() + data class RoomUpgradeSuccess(val replacementRoom: String): RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 8307e93576..156d3f713f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -51,6 +51,7 @@ import androidx.core.view.ViewCompat import androidx.core.view.forEach import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.fragment.app.setFragmentResultListener import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -147,6 +148,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever +import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan @@ -306,6 +308,15 @@ class RoomDetailFragment @Inject constructor( private lateinit var emojiPopup: EmojiPopup + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle -> + bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId -> + roomDetailViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId)) + } + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) @@ -405,6 +416,8 @@ class RoomDetailFragment @Inject constructor( is RoomDetailViewEvents.StartChatEffect -> handleChatEffect(it.type) RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) + RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() + is RoomDetailViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) }.exhaustive } @@ -423,6 +436,19 @@ class RoomDetailFragment @Inject constructor( startActivity(intent) } + private fun handleRoomReplacement() { + // this will join a new room, it can take time and might fail + // so we need to report progress and retry + val tag = JoinReplacementRoomBottomSheet::javaClass.name + JoinReplacementRoomBottomSheet().show(childFragmentManager, tag) + } + + private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: RoomDetailViewEvents.ShowRoomUpgradeDialog) { + val tag = MigrateRoomBottomSheet::javaClass.name + MigrateRoomBottomSheet.newInstance(roomDetailArgs.roomId, roomDetailViewEvents.newVersion) + .show(parentFragmentManager, tag) + } + private fun handleChatEffect(chatEffect: ChatEffect) { when (chatEffect) { ChatEffect.CONFETTI -> { @@ -1306,16 +1332,14 @@ class RoomDetailFragment @Inject constructor( private fun renderTombstoneEventHandling(async: Async) { when (async) { is Loading -> { - // TODO Better handling progress - vectorBaseActivity.showWaitingView(getString(R.string.joining_room)) + // shown in bottom sheet } is Success -> { navigator.openRoom(vectorBaseActivity, async()) vectorBaseActivity.finish() } is Fail -> { - vectorBaseActivity.hideWaitingView() - vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error)) + // shown in bottom sheet } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 4d1e62da7e..458549bbac 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -94,4 +94,6 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() object StopChatEffects : RoomDetailViewEvents() + object RoomReplacementStarted : RoomDetailViewEvents() + data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean): RoomDetailViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 751114c2d9..08e1d215c2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -321,6 +321,11 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() RoomDetailAction.ResendAll -> handleResendAll() + is RoomDetailAction.RoomUpgradeSuccess -> { + setState { + copy(tombstoneEventHandling = Success(action.replacementRoom)) + } + } }.exhaustive } @@ -585,6 +590,11 @@ class RoomDetailViewModel @AssistedInject constructor( val viaServers = MatrixPatterns.extractServerNameFromId(action.event.senderId) ?.let { listOf(it) } .orEmpty() + // need to provide feedback as joining could take some time + _viewEvents.post(RoomDetailViewEvents.RoomReplacementStarted) + setState { + copy(tombstoneEventHandling = Loading()) + } viewModelScope.launch { val result = runCatchingToAsync { session.joinRoom(roomId, viaServers = viaServers) @@ -817,6 +827,23 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) popDraft() } + is ParsedCommand.UpgradeRoom -> { + _viewEvents.post( + RoomDetailViewEvents.ShowRoomUpgradeDialog( + slashCommandResult.newVersion, + room.roomSummary()?.isPublic ?: false + ) + ) +// session.coroutineScope.launch { +// try { +// room.upgradeToVersion(slashCommandResult.newVersion) +// } catch (failure: Throwable) { +// _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) +// } +// } + _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) + popDraft() + } }.exhaustive } is SendMode.EDIT -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomAction.kt new file mode 100644 index 0000000000..cb65be7e28 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.upgrade + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class MigrateRoomAction : VectorViewModelAction { + data class SetAutoInvite(val autoInvite: Boolean) : MigrateRoomAction() + data class SetUpdateKnownParentSpace(val update: Boolean) : MigrateRoomAction() + object UpgradeRoom : MigrateRoomAction() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt new file mode 100644 index 0000000000..12a0fb222b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.upgrade + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.setFragmentResult +import com.airbnb.epoxy.OnModelBuildFinishedListener +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetGenericListBinding +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +class MigrateRoomBottomSheet : + VectorBaseBottomSheetDialogFragment(), + MigrateRoomViewModel.Factory, MigrateRoomController.InteractionListener { + + @Parcelize + data class Args( + val roomId: String, + val newVersion: String + ) : Parcelable + + @Inject + lateinit var viewModelFactory: MigrateRoomViewModel.Factory + + override val showExpanded = true + + @Inject + lateinit var epoxyController: MigrateRoomController + + val viewModel: MigrateRoomViewModel by fragmentViewModel() + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + BottomSheetGenericListBinding.inflate(inflater, container, false) + + override fun invalidate() = withState(viewModel) { state -> + epoxyController.setData(state) + super.invalidate() + + when (val result = state.upgradingStatus) { + is Success -> { + val result = result.invoke() + if (result is UpgradeRoomViewModelTask.Result.Success) { + setFragmentResult(REQUEST_KEY, Bundle().apply { + putString(BUNDLE_KEY_REPLACEMENT_ROOM, result.replacementRoomId) + }) + dismiss() + } + } + } + } + + val postBuild = OnModelBuildFinishedListener { + view?.post { forceExpandState() } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + epoxyController.callback = this + views.bottomSheetRecyclerView.configureWith(epoxyController) + epoxyController.addModelBuildListener(postBuild) + } + + override fun onDestroyView() { + views.bottomSheetRecyclerView.cleanup() + epoxyController.removeModelBuildListener(postBuild) + super.onDestroyView() + } + + override fun create(initialState: MigrateRoomViewState): MigrateRoomViewModel { + return viewModelFactory.create(initialState) + } + + companion object { + + const val REQUEST_KEY = "MigrateRoomBottomSheetRequest" + const val BUNDLE_KEY_REPLACEMENT_ROOM = "BUNDLE_KEY_REPLACEMENT_ROOM" + + fun newInstance(roomId: String, newVersion: String) + : MigrateRoomBottomSheet { + return MigrateRoomBottomSheet().apply { + setArguments(Args(roomId, newVersion)) + } + } + } + + override fun onAutoInvite(autoInvite: Boolean) { + viewModel.handle(MigrateRoomAction.SetAutoInvite(autoInvite)) + } + + override fun onAutoUpdateParent(update: Boolean) { + viewModel.handle(MigrateRoomAction.SetUpdateKnownParentSpace(update)) + } + + override fun onConfirmUpgrade() { + viewModel.handle(MigrateRoomAction.UpgradeRoom) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt new file mode 100644 index 0000000000..8d037860d2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.upgrade + +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import im.vector.app.R +import im.vector.app.core.epoxy.errorWithRetryItem +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.bottomsheet.bottomSheetTitleItem +import im.vector.app.core.ui.list.ItemStyle +import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.core.ui.list.genericProgressBarItem +import im.vector.app.features.form.formSubmitButtonItem +import im.vector.app.features.form.formSwitchItem +import javax.inject.Inject + +class MigrateRoomController @Inject constructor( + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter +) : TypedEpoxyController() { + + interface InteractionListener { + fun onAutoInvite(autoInvite: Boolean) + fun onAutoUpdateParent(update: Boolean) + fun onConfirmUpgrade() + } + + var callback: InteractionListener? = null + + override fun buildModels(data: MigrateRoomViewState?) { + data ?: return + + val host = this@MigrateRoomController + + bottomSheetTitleItem { + id("title") + title( + host.stringProvider.getString(if (data.isPublic) R.string.upgrade_public_room else R.string.upgrade_private_room) + ) + } + + genericFooterItem { + id("warning_text") + centered(false) + style(ItemStyle.NORMAL_TEXT) + text(host.stringProvider.getString(R.string.upgrade_room_warning)) + } + + genericFooterItem { + id("from_to_room") + centered(false) + style(ItemStyle.NORMAL_TEXT) + text(host.stringProvider.getString(R.string.upgrade_public_room_from_to, data.currentVersion, data.newVersion)) + } + + if (!data.isPublic && data.otherMemberCount > 0) { + formSwitchItem { + id("auto_invite") + switchChecked(data.shouldIssueInvites) + title(host.stringProvider.getString(R.string.upgrade_room_auto_invite)) + listener { switch -> host.callback?.onAutoInvite(switch) } + } + } + + if (data.knownParents.isNotEmpty()) { + formSwitchItem { + id("update_parent") + switchChecked(data.shouldUpdateKnownParents) + title(host.stringProvider.getString(R.string.upgrade_room_update_parent)) + listener { switch -> host.callback?.onAutoUpdateParent(switch) } + } + } + when (data.upgradingStatus) { + is Loading -> { + genericProgressBarItem { + id("upgrade_progress") + indeterminate(data.upgradingProgressIndeterminate) + progress(data.upgradingProgress) + total(data.upgradingProgressTotal) + } + } + is Success -> { + when (val result = data.upgradingStatus.invoke()) { + is UpgradeRoomViewModelTask.Result.Failure -> { + val errorText = when (result) { + is UpgradeRoomViewModelTask.Result.UnknownRoom -> { + // should not happen + host.stringProvider.getString(R.string.unknown_error) + } + is UpgradeRoomViewModelTask.Result.NotAllowed -> { + host.stringProvider.getString(R.string.upgrade_room_no_power_to_manage) + } + is UpgradeRoomViewModelTask.Result.ErrorFailure -> { + host.errorFormatter.toHumanReadable(result.throwable) + } + else -> null + } + errorWithRetryItem { + id("error") + text(errorText) + listener { host.callback?.onConfirmUpgrade() } + } + } + is UpgradeRoomViewModelTask.Result.Success -> { + // nop, dismisses + } + } + } + else -> { + formSubmitButtonItem { + id("migrate") + buttonTitleId(R.string.upgrade) + buttonClickListener { host.callback?.onConfirmUpgrade() } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt new file mode 100644 index 0000000000..be9dc1bc52 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.upgrade + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.ViewModelContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session + +class MigrateRoomViewModel @AssistedInject constructor( + @Assisted initialState: MigrateRoomViewState, + private val session: Session, + private val upgradeRoomViewModelTask: UpgradeRoomViewModelTask) + : VectorViewModel(initialState) { + + init { + val room = session.getRoom(initialState.roomId) + val summary = session.getRoomSummary(initialState.roomId) + setState { + copy( + currentVersion = room?.getRoomVersion(), + isPublic = summary?.isPublic ?: false, + otherMemberCount = summary?.otherMemberIds?.count() ?: 0, + knownParents = summary?.flattenParentIds ?: emptyList() + ) + } + } + + @AssistedFactory + interface Factory { + fun create(initialState: MigrateRoomViewState): MigrateRoomViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: MigrateRoomViewState): MigrateRoomViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + override fun handle(action: MigrateRoomAction) { + when (action) { + is MigrateRoomAction.SetAutoInvite -> { + setState { + copy(shouldIssueInvites = action.autoInvite) + } + } + is MigrateRoomAction.SetUpdateKnownParentSpace -> { + setState { + copy(shouldUpdateKnownParents = action.update) + } + } + MigrateRoomAction.UpgradeRoom -> { + handleUpgradeRoom(action) + } + } + } + + val upgradingProgress: ((indeterminate: Boolean, progress: Int, total: Int) -> Unit) = { indeterminate, progress, total -> + setState { + copy( + upgradingProgress = progress, + upgradingProgressTotal = total, + upgradingProgressIndeterminate = indeterminate + ) + } + } + + private fun handleUpgradeRoom(action: MigrateRoomAction) = withState { state -> + val summary = session.getRoomSummary(state.roomId) + setState { + copy(upgradingStatus = Loading()) + } + session.coroutineScope.launch { + val result = upgradeRoomViewModelTask.execute(UpgradeRoomViewModelTask.Params( + roomId = state.roomId, + newVersion = state.newVersion, + userIdsToAutoInvite = summary?.otherMemberIds?.takeIf { state.shouldIssueInvites } ?: emptyList(), + parentSpaceToUpdate = summary?.flattenParentIds?.takeIf { state.shouldUpdateKnownParents } ?: emptyList(), + progressReporter = upgradingProgress + )) + + setState { + copy(upgradingStatus = Success(result)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt new file mode 100644 index 0000000000..78c280fb10 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.upgrade + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized + +data class MigrateRoomViewState( + val roomId: String, + val newVersion: String, + val currentVersion: String? = null, + val isPublic: Boolean = false, + val shouldIssueInvites: Boolean = false, + val shouldUpdateKnownParents: Boolean = false, + val otherMemberCount: Int = 0, + val knownParents: List = emptyList(), + val upgradingStatus: Async = Uninitialized, + val upgradingProgress: Int = 0, + val upgradingProgressTotal: Int = 0, + val upgradingProgressIndeterminate: Boolean = true +) : MvRxState { + constructor(args: MigrateRoomBottomSheet.Args) : this( + roomId = args.roomId, + newVersion = args.newVersion + ) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt new file mode 100644 index 0000000000..32c8e6ee92 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.upgrade + +import im.vector.app.core.platform.ViewModelTask +import im.vector.app.core.resources.StringProvider +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.Session +import timber.log.Timber +import javax.inject.Inject + +class UpgradeRoomViewModelTask @Inject constructor( + val session: Session, + val stringProvider: StringProvider +) : ViewModelTask { + + sealed class Result { + data class Success(val replacementRoomId: String) : Result() + abstract class Failure(val throwable: Throwable?) : Result() + object UnknownRoom : Failure(null) + object NotAllowed : Failure(null) + class ErrorFailure(throwable: Throwable) : Failure(throwable) + } + + data class Params( + val roomId: String, + val newVersion: String, + val userIdsToAutoInvite: List = emptyList(), + val parentSpaceToUpdate: List = emptyList(), + val progressReporter: ((indeterminate: Boolean, progress: Int, total: Int) -> Unit)? = null + ) + + override suspend fun execute(params: Params): Result { + params.progressReporter?.invoke(true, 0, 0) + + val room = session.getRoom(params.roomId) + ?: return Result.UnknownRoom + if (!room.userMayUpgradeRoom(session.myUserId)) { + return Result.NotAllowed + } + + val updatedRoomId = try { + room.upgradeToVersion(params.newVersion) + } catch (failure: Throwable) { + return Result.ErrorFailure(failure) + } + + val totalStep = params.userIdsToAutoInvite.size + params.parentSpaceToUpdate.size + var currentStep = 0 + params.userIdsToAutoInvite.forEach { + params.progressReporter?.invoke(false, currentStep, totalStep) + tryOrNull { + session.getRoom(updatedRoomId)?.invite(it) + } + currentStep++ + } + + params.parentSpaceToUpdate.forEach { parentId -> + params.progressReporter?.invoke(false, currentStep, totalStep) + // we try and silently fail + try { + session.getRoom(parentId)?.asSpace()?.let { parentSpace -> + val currentInfo = parentSpace.getChildInfo(params.roomId) + if (currentInfo != null) { + parentSpace.addChildren( + roomId = updatedRoomId, + viaServers = currentInfo.via, + order = currentInfo.order, + autoJoin = currentInfo.autoJoin ?: false, + suggested = currentInfo.suggested + ) + + parentSpace.removeChildren(params.roomId) + } + } + } catch (failure: Throwable) { + Timber.d("## Migrate: Failed to update space parent. cause: ${failure.localizedMessage}") + } finally { + currentStep++ + } + } + + return Result.Success(updatedRoomId) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt index 3217756a82..37f95b184e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt @@ -26,16 +26,20 @@ import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericWithValueItem import im.vector.app.features.discovery.settingsCenteredImageItem import im.vector.app.features.discovery.settingsInfoItem import im.vector.app.features.discovery.settingsSectionTitleItem +import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.federation.FederationVersion import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.homeserver.RoomVersionStatus import javax.inject.Inject class HomeserverSettingsController @Inject constructor( private val stringProvider: StringProvider, - private val errorFormatter: ErrorFormatter + private val errorFormatter: ErrorFormatter, + private val vectorPreferences: VectorPreferences ) : TypedEpoxyController() { var callback: Callback? = null @@ -118,5 +122,36 @@ class HomeserverSettingsController @Inject constructor( helperText(host.stringProvider.getString(R.string.settings_server_upload_size_content, "${limit / 1048576L} MB")) } } + + if (vectorPreferences.developerMode()) { + val roomCapabilities = data.homeServerCapabilities.roomVersions + if (roomCapabilities != null) { + settingsSectionTitleItem { + id("room_versions") + titleResId(R.string.settings_server_room_versions) + } + + genericWithValueItem { + id("room_version_default") + title(host.stringProvider.getString(R.string.settings_server_default_room_version)) + value(roomCapabilities.defaultRoomVersion) + } + + roomCapabilities.supportedVersion.forEach { + genericWithValueItem { + id("room_version_${it.version}") + title(it.version) + value( + host.stringProvider.getString( + when (it.status) { + RoomVersionStatus.STABLE -> R.string.settings_server_room_version_stable + RoomVersionStatus.UNSTABLE -> R.string.settings_server_room_version_unstable + } + ) + ) + } + } + } + } } } diff --git a/vector/src/main/res/layout/bottom_sheet_tombstone_join.xml b/vector/src/main/res/layout/bottom_sheet_tombstone_join.xml new file mode 100644 index 0000000000..3e7fd495a9 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_tombstone_join.xml @@ -0,0 +1,48 @@ + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_generic_progress.xml b/vector/src/main/res/layout/item_generic_progress.xml new file mode 100644 index 0000000000..29ad67efa0 --- /dev/null +++ b/vector/src/main/res/layout/item_generic_progress.xml @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_create.xml b/vector/src/main/res/layout/item_timeline_event_create.xml index ea881ccdd0..51da203d10 100644 --- a/vector/src/main/res/layout/item_timeline_event_create.xml +++ b/vector/src/main/res/layout/item_timeline_event_create.xml @@ -10,7 +10,7 @@ style="@style/Widget.Vector.TextView.Body" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" + android:layout_marginStart="0dp" android:layout_marginTop="16dp" android:layout_marginBottom="16dp" android:background="?vctr_keys_backup_banner_accent_color" @@ -18,7 +18,7 @@ android:gravity="center|start" android:minHeight="80dp" android:padding="16dp" - app:drawableStartCompat="@drawable/error" + app:drawableStartCompat="@drawable/ic_warning_badge" tools:text="This room is continuation…" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/view_notification_area.xml b/vector/src/main/res/layout/view_notification_area.xml index 5e3a79291c..1d30b87aa4 100644 --- a/vector/src/main/res/layout/view_notification_area.xml +++ b/vector/src/main/res/layout/view_notification_area.xml @@ -1,38 +1,34 @@ - - - + android:orientation="horizontal" + android:padding="16dp"> + tools:src="@drawable/ic_warning_badge" /> - \ No newline at end of file + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 54356db664..9305deb752 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -926,7 +926,7 @@ Resend unsent messages Delete unsent messages File not found - You do not have permission to post to this room + You do not have permission to post to this room. %d new message %d new messages @@ -1820,7 +1820,7 @@ Please enter a username. Please enter your password. - This room has been replaced and is no longer active + This room has been replaced and is no longer active. The conversation continues here This room is a continuation of another conversation Click here to see older messages @@ -2735,6 +2735,11 @@ Server file upload limit Your homeserver accepts attachments (files, media, etc.) with a size up to %s. The limit is unknown. + + Room Versions 🕶 + Default Version + stable + unstable No cryptographic information available @@ -3295,6 +3300,7 @@ Create a Space Join the Space with the given id Leave room with given id (or current room if null) + Upgrades a room to a new version Sending Sent @@ -3406,4 +3412,19 @@ "Teammate spaces aren’t quite ready but you can still give them a try" "At the moment people might not be able to join any private rooms you make.\n\nWe’ll be improving this as part of the beta, but just wanted to let you know." - + + Join replacement room + Please be patient, it may take some time. + + + Upgrade + Upgrade public room + Upgrade private room + Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.\nThis usually only affects how the room is processed on the server. + You\'ll upgrade this room from %s to %s. + Automatically invite users + Automatically update space parent + Automatically update parent space + You need permission to upgrade a room + + From 57c75f80399e0f4bb1ba24c9b08c0eede3fc5b26 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 24 Jun 2021 09:38:20 +0200 Subject: [PATCH 004/339] Ugrade unstable room notice in settings default update parent, clean migrate bottomsheet layout --- .../room/version/RoomVersionService.kt | 5 +- .../internal/session/room/RoomUpgradeBody.kt | 2 +- .../session/room/RoomUpgradeResponse.kt | 2 +- .../room/version/DefaultRoomVersionService.kt | 17 ++- .../room/version/RoomVersionUpgradeTask.kt | 2 +- tools/check/forbidden_strings_in_code.txt | 2 +- .../app/features/command/CommandParser.kt | 6 +- .../detail/upgrade/MigrateRoomBottomSheet.kt | 112 +++++++++------ .../detail/upgrade/MigrateRoomController.kt | 135 ------------------ .../detail/upgrade/MigrateRoomViewState.kt | 2 +- .../roomprofile/RoomProfileController.kt | 28 +++- .../roomprofile/RoomProfileFragment.kt | 6 + .../roomprofile/RoomProfileViewModel.kt | 12 +- .../roomprofile/RoomProfileViewState.kt | 6 +- .../res/layout/bottom_sheet_generic_list.xml | 1 + .../res/layout/bottom_sheet_room_upgrade.xml | 90 ++++++++++++ vector/src/main/res/values/strings.xml | 3 + 17 files changed, 239 insertions(+), 192 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt create mode 100644 vector/src/main/res/layout/bottom_sheet_room_upgrade.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt index a90e1343f2..08dd955394 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,8 @@ interface RoomVersionService { */ suspend fun upgradeToVersion(version: String): String - suspend fun getRecommendedVersion() : String + fun getRecommendedVersion() : String fun userMayUpgradeRoom(userId: String): Boolean + fun isUsingUnstableRoomVersion(): Boolean } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt index feefbe5f66..4629f6e409 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeBody.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.kt index 16e95194a3..1cca2c572b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomUpgradeResponse.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt index 1b5d62926e..9d253e8b1c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import io.realm.Realm import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.homeserver.RoomVersionStatus import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper @@ -63,7 +64,7 @@ internal class DefaultRoomVersionService @AssistedInject constructor( ) } - override suspend fun getRecommendedVersion(): String { + override fun getRecommendedVersion(): String { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> HomeServerCapabilitiesEntity.get(realm)?.let { HomeServerCapabilitiesMapper.map(it) @@ -71,6 +72,18 @@ internal class DefaultRoomVersionService @AssistedInject constructor( } } + override fun isUsingUnstableRoomVersion(): Boolean { + var isUsingUnstable: Boolean + Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val versionCaps = HomeServerCapabilitiesEntity.get(realm)?.let { + HomeServerCapabilitiesMapper.map(it) + }?.roomVersions + val currentVersion = getRoomVersion() + isUsingUnstable = versionCaps?.supportedVersion?.firstOrNull { it.version == currentVersion }?.status == RoomVersionStatus.UNSTABLE + } + return isUsingUnstable + } + override fun userMayUpgradeRoom(userId: String): Boolean { val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) ?.content?.toModel() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt index 444795b5a9..457bb3e948 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/RoomVersionUpgradeTask.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index e0645e00b3..ba9dec0877 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -162,7 +162,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===101 +enum class===102 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 224956049c..adba6e4a18 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -332,7 +332,11 @@ object CommandParser { } Command.UPGRADE_ROOM.command -> { val newVersion = textMessage.substring(Command.UPGRADE_ROOM.command.length).trim() - ParsedCommand.UpgradeRoom(newVersion) + if (newVersion.isEmpty()) { + ParsedCommand.ErrorSyntax(Command.UPGRADE_ROOM) + } else { + ParsedCommand.UpgradeRoom(newVersion) + } } else -> { // Unknown command diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt index 12a0fb222b..0de905d059 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt @@ -21,22 +21,24 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.fragment.app.setFragmentResult -import com.airbnb.epoxy.OnModelBuildFinishedListener +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import im.vector.app.R import im.vector.app.core.di.ScreenComponent -import im.vector.app.core.extensions.cleanup -import im.vector.app.core.extensions.configureWith +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment -import im.vector.app.databinding.BottomSheetGenericListBinding +import im.vector.app.databinding.BottomSheetRoomUpgradeBinding import kotlinx.parcelize.Parcelize import javax.inject.Inject class MigrateRoomBottomSheet : - VectorBaseBottomSheetDialogFragment(), - MigrateRoomViewModel.Factory, MigrateRoomController.InteractionListener { + VectorBaseBottomSheetDialogFragment(), + MigrateRoomViewModel.Factory { @Parcelize data class Args( @@ -50,7 +52,7 @@ class MigrateRoomBottomSheet : override val showExpanded = true @Inject - lateinit var epoxyController: MigrateRoomController + lateinit var errorFormatter: ErrorFormatter val viewModel: MigrateRoomViewModel by fragmentViewModel() @@ -58,41 +60,79 @@ class MigrateRoomBottomSheet : injector.inject(this) } - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = - BottomSheetGenericListBinding.inflate(inflater, container, false) - override fun invalidate() = withState(viewModel) { state -> - epoxyController.setData(state) - super.invalidate() - when (val result = state.upgradingStatus) { + views.headerText.setText(if (state.isPublic) R.string.upgrade_public_room else R.string.upgrade_private_room) + views.upgradeFromTo.text = getString(R.string.upgrade_public_room_from_to, state.currentVersion, state.newVersion) + + views.autoInviteSwitch.isVisible = !state.isPublic && state.otherMemberCount > 0 + + views.autoUpdateParent.isVisible = state.knownParents.isNotEmpty() + + when (state.upgradingStatus) { + is Loading -> { + views.progressBar.isVisible = true + views.progressBar.isIndeterminate = state.upgradingProgressIndeterminate + views.progressBar.progress = state.upgradingProgress + views.progressBar.max = state.upgradingProgressTotal + views.inlineError.setTextOrHide(null) + views.button.isVisible = false + } is Success -> { - val result = result.invoke() - if (result is UpgradeRoomViewModelTask.Result.Success) { - setFragmentResult(REQUEST_KEY, Bundle().apply { - putString(BUNDLE_KEY_REPLACEMENT_ROOM, result.replacementRoomId) - }) - dismiss() + views.progressBar.isVisible = false + when (val result = state.upgradingStatus.invoke()) { + is UpgradeRoomViewModelTask.Result.Failure -> { + val errorText = when (result) { + is UpgradeRoomViewModelTask.Result.UnknownRoom -> { + // should not happen + getString(R.string.unknown_error) + } + is UpgradeRoomViewModelTask.Result.NotAllowed -> { + getString(R.string.upgrade_room_no_power_to_manage) + } + is UpgradeRoomViewModelTask.Result.ErrorFailure -> { + errorFormatter.toHumanReadable(result.throwable) + } + else -> null + } + views.inlineError.setTextOrHide(errorText) + views.button.isVisible = true + views.button.text = getString(R.string.global_retry) + } + is UpgradeRoomViewModelTask.Result.Success -> { + setFragmentResult(REQUEST_KEY, Bundle().apply { + putString(BUNDLE_KEY_REPLACEMENT_ROOM, result.replacementRoomId) + }) + dismiss() + } } } + else -> { + views.button.isVisible = true + views.button.text = getString(R.string.upgrade) + } } + + super.invalidate() } - val postBuild = OnModelBuildFinishedListener { - view?.post { forceExpandState() } - } + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + BottomSheetRoomUpgradeBinding.inflate(inflater, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - epoxyController.callback = this - views.bottomSheetRecyclerView.configureWith(epoxyController) - epoxyController.addModelBuildListener(postBuild) - } - override fun onDestroyView() { - views.bottomSheetRecyclerView.cleanup() - epoxyController.removeModelBuildListener(postBuild) - super.onDestroyView() + views.button.debouncedClicks { + viewModel.handle(MigrateRoomAction.UpgradeRoom) + } + + views.autoInviteSwitch.setOnCheckedChangeListener { _, isChecked -> + viewModel.handle(MigrateRoomAction.SetAutoInvite(isChecked)) + } + + views.autoUpdateParent.setOnCheckedChangeListener { _, isChecked -> + viewModel.handle(MigrateRoomAction.SetUpdateKnownParentSpace(isChecked)) + } } override fun create(initialState: MigrateRoomViewState): MigrateRoomViewModel { @@ -111,16 +151,4 @@ class MigrateRoomBottomSheet : } } } - - override fun onAutoInvite(autoInvite: Boolean) { - viewModel.handle(MigrateRoomAction.SetAutoInvite(autoInvite)) - } - - override fun onAutoUpdateParent(update: Boolean) { - viewModel.handle(MigrateRoomAction.SetUpdateKnownParentSpace(update)) - } - - override fun onConfirmUpgrade() { - viewModel.handle(MigrateRoomAction.UpgradeRoom) - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt deleted file mode 100644 index 8d037860d2..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomController.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2021 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.home.room.detail.upgrade - -import com.airbnb.epoxy.TypedEpoxyController -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success -import im.vector.app.R -import im.vector.app.core.epoxy.errorWithRetryItem -import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.resources.StringProvider -import im.vector.app.core.ui.bottomsheet.bottomSheetTitleItem -import im.vector.app.core.ui.list.ItemStyle -import im.vector.app.core.ui.list.genericFooterItem -import im.vector.app.core.ui.list.genericProgressBarItem -import im.vector.app.features.form.formSubmitButtonItem -import im.vector.app.features.form.formSwitchItem -import javax.inject.Inject - -class MigrateRoomController @Inject constructor( - private val stringProvider: StringProvider, - private val errorFormatter: ErrorFormatter -) : TypedEpoxyController() { - - interface InteractionListener { - fun onAutoInvite(autoInvite: Boolean) - fun onAutoUpdateParent(update: Boolean) - fun onConfirmUpgrade() - } - - var callback: InteractionListener? = null - - override fun buildModels(data: MigrateRoomViewState?) { - data ?: return - - val host = this@MigrateRoomController - - bottomSheetTitleItem { - id("title") - title( - host.stringProvider.getString(if (data.isPublic) R.string.upgrade_public_room else R.string.upgrade_private_room) - ) - } - - genericFooterItem { - id("warning_text") - centered(false) - style(ItemStyle.NORMAL_TEXT) - text(host.stringProvider.getString(R.string.upgrade_room_warning)) - } - - genericFooterItem { - id("from_to_room") - centered(false) - style(ItemStyle.NORMAL_TEXT) - text(host.stringProvider.getString(R.string.upgrade_public_room_from_to, data.currentVersion, data.newVersion)) - } - - if (!data.isPublic && data.otherMemberCount > 0) { - formSwitchItem { - id("auto_invite") - switchChecked(data.shouldIssueInvites) - title(host.stringProvider.getString(R.string.upgrade_room_auto_invite)) - listener { switch -> host.callback?.onAutoInvite(switch) } - } - } - - if (data.knownParents.isNotEmpty()) { - formSwitchItem { - id("update_parent") - switchChecked(data.shouldUpdateKnownParents) - title(host.stringProvider.getString(R.string.upgrade_room_update_parent)) - listener { switch -> host.callback?.onAutoUpdateParent(switch) } - } - } - when (data.upgradingStatus) { - is Loading -> { - genericProgressBarItem { - id("upgrade_progress") - indeterminate(data.upgradingProgressIndeterminate) - progress(data.upgradingProgress) - total(data.upgradingProgressTotal) - } - } - is Success -> { - when (val result = data.upgradingStatus.invoke()) { - is UpgradeRoomViewModelTask.Result.Failure -> { - val errorText = when (result) { - is UpgradeRoomViewModelTask.Result.UnknownRoom -> { - // should not happen - host.stringProvider.getString(R.string.unknown_error) - } - is UpgradeRoomViewModelTask.Result.NotAllowed -> { - host.stringProvider.getString(R.string.upgrade_room_no_power_to_manage) - } - is UpgradeRoomViewModelTask.Result.ErrorFailure -> { - host.errorFormatter.toHumanReadable(result.throwable) - } - else -> null - } - errorWithRetryItem { - id("error") - text(errorText) - listener { host.callback?.onConfirmUpgrade() } - } - } - is UpgradeRoomViewModelTask.Result.Success -> { - // nop, dismisses - } - } - } - else -> { - formSubmitButtonItem { - id("migrate") - buttonTitleId(R.string.upgrade) - buttonClickListener { host.callback?.onConfirmUpgrade() } - } - } - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt index 78c280fb10..e3936de42f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt @@ -26,7 +26,7 @@ data class MigrateRoomViewState( val currentVersion: String? = null, val isPublic: Boolean = false, val shouldIssueInvites: Boolean = false, - val shouldUpdateKnownParents: Boolean = false, + val shouldUpdateKnownParents: Boolean = true, val otherMemberCount: Int = 0, val knownParents: List = emptyList(), val upgradingStatus: Async = Uninitialized, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index aa2db348c8..106062f7b3 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -22,8 +22,10 @@ import im.vector.app.R import im.vector.app.core.epoxy.expandableTextItem import im.vector.app.core.epoxy.profiles.buildProfileAction import im.vector.app.core.epoxy.profiles.buildProfileSection +import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.core.ui.list.genericPositiveButtonItem import im.vector.app.features.home.ShortcutCreator import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod @@ -34,6 +36,7 @@ import javax.inject.Inject class RoomProfileController @Inject constructor( private val stringProvider: StringProvider, + private val colorProvider: ColorProvider, private val vectorPreferences: VectorPreferences, private val shortcutCreator: ShortcutCreator ) : TypedEpoxyController() { @@ -55,6 +58,7 @@ class RoomProfileController @Inject constructor( fun onRoomIdClicked() fun onRoomDevToolsClicked() fun onUrlInTopicLongClicked(url: String) + fun doMigrateToVersion(newVersion: String) } override fun buildModels(data: RoomProfileViewState?) { @@ -87,6 +91,28 @@ class RoomProfileController @Inject constructor( // Security buildProfileSection(stringProvider.getString(R.string.room_profile_section_security)) + + // Upgrade warning + val roomVersion = data.roomCreateContent()?.roomVersion + if (data.canUpgradeRoom + && !data.isTombstoned + && roomVersion != null + && data.isUsingUnstableRoomVersion + && data.recommendedRoomVersion != null) { + genericFooterItem { + id("version_warning") + text(host.stringProvider.getString(R.string.room_using_unstable_room_version, roomVersion)) + textColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) + centered(false) + } + + genericPositiveButtonItem { + id("migrate_button") + text(host.stringProvider.getString(R.string.room_upgrade_to_recommened_version)) + buttonClickAction { host.callback?.doMigrateToVersion(data.recommendedRoomVersion) } + } + } + val learnMoreSubtitle = if (roomSummary.isEncrypted) { if (roomSummary.isDirect) R.string.direct_room_profile_encrypted_subtitle else R.string.room_profile_encrypted_subtitle } else { @@ -194,7 +220,7 @@ class RoomProfileController @Inject constructor( editable = false, action = { callback?.onRoomIdClicked() } ) - data.roomCreateContent()?.roomVersion?.let { + roomVersion?.let { buildProfileAction( id = "roomVersion", title = stringProvider.getString(R.string.room_settings_room_version_title), diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index f9324ad3e2..8a10bcc842 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -45,6 +45,7 @@ import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.FragmentMatrixProfileBinding import im.vector.app.databinding.ViewStubRoomProfileHeaderBinding import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.list.actions.RoomListActionsArgs import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction @@ -303,6 +304,11 @@ class RoomProfileFragment @Inject constructor( copyToClipboard(requireContext(), url, true) } + override fun doMigrateToVersion(newVersion: String) { + MigrateRoomBottomSheet.newInstance(roomProfileArgs.roomId, newVersion) + .show(parentFragmentManager, "migrate") + } + private fun onShareRoomProfile(permalink: String) { startSharePlainTextIntent( fragment = this, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt index 209ebcc35b..9fee87880a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt @@ -22,8 +22,8 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -81,8 +81,14 @@ class RoomProfileViewModel @AssistedInject constructor( rxRoom.liveStateEvent(EventType.STATE_ROOM_CREATE, QueryStringValue.NoCondition) .mapOptional { it.content.toModel() } .unwrap() - .execute { - copy(roomCreateContent = it) + .execute { async -> + copy( + roomCreateContent = async, + recommendedRoomVersion = room.getRecommendedVersion(), + isUsingUnstableRoomVersion = room.isUsingUnstableRoomVersion(), + canUpgradeRoom = room.userMayUpgradeRoom(session.myUserId), + isTombstoned = room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE) != null + ) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt index bf7cd732ef..999b6540bd 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt @@ -30,7 +30,11 @@ data class RoomProfileViewState( val roomCreateContent: Async = Uninitialized, val bannedMembership: Async> = Uninitialized, val actionPermissions: ActionPermissions = ActionPermissions(), - val isLoading: Boolean = false + val isLoading: Boolean = false, + val isUsingUnstableRoomVersion: Boolean = false, + val recommendedRoomVersion: String? = null, + val canUpgradeRoom: Boolean = false, + val isTombstoned: Boolean = false ) : MvRxState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/res/layout/bottom_sheet_generic_list.xml b/vector/src/main/res/layout/bottom_sheet_generic_list.xml index 87a2cb54fc..966ad48b25 100644 --- a/vector/src/main/res/layout/bottom_sheet_generic_list.xml +++ b/vector/src/main/res/layout/bottom_sheet_generic_list.xml @@ -6,6 +6,7 @@ android:layout_height="match_parent" android:background="?colorSurface" android:fadeScrollbars="false" + android:nestedScrollingEnabled="false" android:scrollbars="vertical" tools:itemCount="5" tools:listitem="@layout/item_bottom_sheet_action" /> diff --git a/vector/src/main/res/layout/bottom_sheet_room_upgrade.xml b/vector/src/main/res/layout/bottom_sheet_room_upgrade.xml new file mode 100644 index 0000000000..3cb1daffad --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_room_upgrade.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + +