[Rich text editor] Add support for links in the rich text editor (#7746)
This commit is contained in:
parent
67e15a42c0
commit
50466792c6
|
@ -0,0 +1 @@
|
||||||
|
[Rich text editor] Add support for links
|
|
@ -98,7 +98,7 @@ ext.libs = [
|
||||||
],
|
],
|
||||||
element : [
|
element : [
|
||||||
'opusencoder' : "io.element.android:opusencoder:1.1.0",
|
'opusencoder' : "io.element.android:opusencoder:1.1.0",
|
||||||
'wysiwyg' : "io.element.android:wysiwyg:0.9.0"
|
'wysiwyg' : "io.element.android:wysiwyg:0.10.0"
|
||||||
],
|
],
|
||||||
squareup : [
|
squareup : [
|
||||||
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
||||||
|
|
|
@ -3479,13 +3479,19 @@
|
||||||
<string name="qr_code_login_confirm_security_code">Confirm</string>
|
<string name="qr_code_login_confirm_security_code">Confirm</string>
|
||||||
<string name="qr_code_login_confirm_security_code_description">Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.</string>
|
<string name="qr_code_login_confirm_security_code_description">Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.</string>
|
||||||
|
|
||||||
<!-- WYSIWYG Composer -->
|
<!-- Rich text editor -->
|
||||||
<string name="rich_text_editor_format_bold">Apply bold format</string>
|
<string name="rich_text_editor_format_bold">Apply bold format</string>
|
||||||
<string name="rich_text_editor_format_italic">Apply italic format</string>
|
<string name="rich_text_editor_format_italic">Apply italic format</string>
|
||||||
<string name="rich_text_editor_format_strikethrough">Apply strikethrough format</string>
|
<string name="rich_text_editor_format_strikethrough">Apply strikethrough format</string>
|
||||||
<string name="rich_text_editor_format_underline">Apply underline format</string>
|
<string name="rich_text_editor_format_underline">Apply underline format</string>
|
||||||
|
<string name="rich_text_editor_link">Set link</string>
|
||||||
<string name="rich_text_editor_full_screen_toggle">Toggle full screen mode</string>
|
<string name="rich_text_editor_full_screen_toggle">Toggle full screen mode</string>
|
||||||
|
|
||||||
|
<string name="set_link_text">Text</string>
|
||||||
|
<string name="set_link_link">Link</string>
|
||||||
|
<string name="set_link_create">Create a link</string>
|
||||||
|
<string name="set_link_edit">Edit link</string>
|
||||||
|
|
||||||
<!-- ReplyTo events -->
|
<!-- ReplyTo events -->
|
||||||
<string name="message_reply_to_prefix">In reply to</string>
|
<string name="message_reply_to_prefix">In reply to</string>
|
||||||
<string name="message_reply_to_sender_sent_file">sent a file.</string>
|
<string name="message_reply_to_sender_sent_file">sent a file.</string>
|
||||||
|
|
|
@ -46,6 +46,7 @@ import im.vector.app.features.home.UserColorAccountDataViewModel
|
||||||
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
|
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
|
||||||
import im.vector.app.features.home.room.detail.TimelineViewModel
|
import im.vector.app.features.home.room.detail.TimelineViewModel
|
||||||
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
|
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
|
||||||
|
import im.vector.app.features.home.room.detail.composer.link.SetLinkViewModel
|
||||||
import im.vector.app.features.home.room.detail.search.SearchViewModel
|
import im.vector.app.features.home.room.detail.search.SearchViewModel
|
||||||
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel
|
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel
|
||||||
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
|
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
|
||||||
|
@ -691,4 +692,9 @@ interface MavericksViewModelModule {
|
||||||
fun vectorSettingsNotificationPreferenceViewModelFactory(
|
fun vectorSettingsNotificationPreferenceViewModelFactory(
|
||||||
factory: VectorSettingsNotificationPreferenceViewModel.Factory
|
factory: VectorSettingsNotificationPreferenceViewModel.Factory
|
||||||
): MavericksAssistedViewModelFactory<*, *>
|
): MavericksAssistedViewModelFactory<*, *>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@MavericksViewModelKey(SetLinkViewModel::class)
|
||||||
|
fun setLinkViewModelFactory(factory: SetLinkViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
/*
|
||||||
|
* 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.platform
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.airbnb.mvrx.MavericksView
|
||||||
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.di.ActivityEntryPoint
|
||||||
|
import im.vector.app.core.extensions.singletonEntryPoint
|
||||||
|
import im.vector.app.core.extensions.toMvRxBundle
|
||||||
|
import im.vector.app.features.analytics.AnalyticsTracker
|
||||||
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Mavericks capabilities, handle DI and bindings.
|
||||||
|
*/
|
||||||
|
abstract class VectorBaseDialogFragment<VB : ViewBinding> : DialogFragment(), MavericksView {
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Analytics
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
protected var analyticsScreenName: MobileScreen.ScreenName? = null
|
||||||
|
|
||||||
|
protected lateinit var analyticsTracker: AnalyticsTracker
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* View
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
private var _binding: VB? = null
|
||||||
|
|
||||||
|
// This property is only valid between onCreateView and onDestroyView.
|
||||||
|
protected val views: VB
|
||||||
|
get() = _binding!!
|
||||||
|
|
||||||
|
abstract fun getBinding(inflater: LayoutInflater, container: ViewGroup?): VB
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* View model
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
private lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||||
|
|
||||||
|
protected val activityViewModelProvider
|
||||||
|
get() = ViewModelProvider(requireActivity(), viewModelFactory)
|
||||||
|
|
||||||
|
protected val fragmentViewModelProvider
|
||||||
|
get() = ViewModelProvider(this, viewModelFactory)
|
||||||
|
|
||||||
|
val vectorBaseActivity: VectorBaseActivity<*> by lazy {
|
||||||
|
activity as VectorBaseActivity<*>
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setStyle(STYLE_NORMAL, ThemeUtils.getApplicationThemeRes(requireContext()))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
_binding = getBinding(inflater, container)
|
||||||
|
return views.root
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onDestroyView() {
|
||||||
|
_binding = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java)
|
||||||
|
viewModelFactory = activityEntryPoint.viewModelFactory()
|
||||||
|
val singletonEntryPoint = context.singletonEntryPoint()
|
||||||
|
analyticsTracker = singletonEntryPoint.analyticsTracker()
|
||||||
|
super.onAttach(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
Timber.i("onResume BottomSheet ${javaClass.simpleName}")
|
||||||
|
analyticsScreenName?.let {
|
||||||
|
analyticsTracker.screen(MobileScreen(screenName = it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
// This ensures that invalidate() is called for static screens that don't
|
||||||
|
// subscribe to a ViewModel.
|
||||||
|
postInvalidate()
|
||||||
|
requireDialog().window?.setWindowAnimations(R.style.Animation_AppCompat_Dialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setArguments(args: Parcelable? = null) {
|
||||||
|
arguments = args.toMvRxBundle()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Views
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
protected fun View.debouncedClicks(onClicked: () -> Unit) {
|
||||||
|
clicks()
|
||||||
|
.onEach { onClicked() }
|
||||||
|
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* ViewEvents
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
|
||||||
|
viewEvents
|
||||||
|
.stream()
|
||||||
|
.onEach {
|
||||||
|
observer(it)
|
||||||
|
}
|
||||||
|
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
|
}
|
||||||
|
}
|
|
@ -80,6 +80,9 @@ import im.vector.app.features.home.room.detail.AutoCompleter
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
|
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
|
||||||
import im.vector.app.features.home.room.detail.TimelineViewModel
|
import im.vector.app.features.home.room.detail.TimelineViewModel
|
||||||
|
import im.vector.app.features.home.room.detail.composer.link.SetLinkFragment
|
||||||
|
import im.vector.app.features.home.room.detail.composer.link.SetLinkSharedAction
|
||||||
|
import im.vector.app.features.home.room.detail.composer.link.SetLinkSharedActionViewModel
|
||||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
||||||
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel
|
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel
|
||||||
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
|
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
|
||||||
|
@ -147,6 +150,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
||||||
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
|
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
|
||||||
private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
|
private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
|
||||||
private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
|
private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
|
||||||
|
private val setLinkActionsViewModel: SetLinkSharedActionViewModel by viewModels()
|
||||||
|
|
||||||
private val composer: MessageComposerView get() {
|
private val composer: MessageComposerView get() {
|
||||||
return if (vectorPreferences.isRichTextEditorEnabled()) {
|
return if (vectorPreferences.isRichTextEditorEnabled()) {
|
||||||
|
@ -212,6 +216,14 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
||||||
.onEach { onTypeSelected(it.attachmentType) }
|
.onEach { onTypeSelected(it.attachmentType) }
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
|
setLinkActionsViewModel.stream()
|
||||||
|
.onEach { when (it) {
|
||||||
|
is SetLinkSharedAction.Insert -> views.richTextComposerLayout.insertLink(it.link, it.text)
|
||||||
|
is SetLinkSharedAction.Set -> views.richTextComposerLayout.setLink(it.link)
|
||||||
|
SetLinkSharedAction.Remove -> views.richTextComposerLayout.removeLink()
|
||||||
|
} }
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
messageComposerViewModel.stateFlow.map { it.isFullScreen }
|
messageComposerViewModel.stateFlow.map { it.isFullScreen }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach { isFullScreen ->
|
.onEach { isFullScreen ->
|
||||||
|
@ -385,6 +397,10 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
||||||
override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state ->
|
override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state ->
|
||||||
messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(!state.isFullScreen))
|
messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(!state.isFullScreen))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSetLink(isTextSupported: Boolean, initialLink: String?) {
|
||||||
|
SetLinkFragment.show(isTextSupported, initialLink, childFragmentManager)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,4 +45,5 @@ interface Callback : ComposerEditText.Callback {
|
||||||
fun onAddAttachment()
|
fun onAddAttachment()
|
||||||
fun onExpandOrCompactChange()
|
fun onExpandOrCompactChange()
|
||||||
fun onFullScreenModeChanged()
|
fun onFullScreenModeChanged()
|
||||||
|
fun onSetLink(isTextSupported: Boolean, initialLink: String?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ import im.vector.app.databinding.ComposerRichTextLayoutBinding
|
||||||
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
|
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
|
||||||
import io.element.android.wysiwyg.EditorEditText
|
import io.element.android.wysiwyg.EditorEditText
|
||||||
import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
|
import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
|
||||||
|
import io.element.android.wysiwyg.inputhandlers.models.LinkAction
|
||||||
import io.element.android.wysiwyg.utils.RustErrorCollector
|
import io.element.android.wysiwyg.utils.RustErrorCollector
|
||||||
import uniffi.wysiwyg_composer.ActionState
|
import uniffi.wysiwyg_composer.ActionState
|
||||||
import uniffi.wysiwyg_composer.ComposerAction
|
import uniffi.wysiwyg_composer.ComposerAction
|
||||||
|
@ -231,8 +232,25 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
|
||||||
addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.STRIKE_THROUGH) {
|
addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.STRIKE_THROUGH) {
|
||||||
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
|
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
|
||||||
}
|
}
|
||||||
|
addRichTextMenuItem(R.drawable.ic_composer_link, R.string.rich_text_editor_link, ComposerAction.LINK) {
|
||||||
|
views.richTextComposerEditText.getLinkAction()?.let {
|
||||||
|
when (it) {
|
||||||
|
LinkAction.InsertLink -> callback?.onSetLink(isTextSupported = true, initialLink = null)
|
||||||
|
is LinkAction.SetLink -> callback?.onSetLink(isTextSupported = false, initialLink = it.currentLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setLink(link: String?) =
|
||||||
|
views.richTextComposerEditText.setLink(link)
|
||||||
|
|
||||||
|
fun insertLink(link: String, text: String) =
|
||||||
|
views.richTextComposerEditText.insertLink(link, text)
|
||||||
|
|
||||||
|
fun removeLink() =
|
||||||
|
views.richTextComposerEditText.removeLink()
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private fun disallowParentInterceptTouchEvent(view: View) {
|
private fun disallowParentInterceptTouchEvent(view: View) {
|
||||||
view.setOnTouchListener { v, event ->
|
view.setOnTouchListener { v, event ->
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.link
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
|
sealed class SetLinkAction : VectorViewModelAction {
|
||||||
|
data class LinkChanged(
|
||||||
|
val newLink: String
|
||||||
|
) : SetLinkAction()
|
||||||
|
|
||||||
|
data class Save(
|
||||||
|
val link: String,
|
||||||
|
val text: String,
|
||||||
|
) : SetLinkAction()
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
/*
|
||||||
|
* 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.link
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.airbnb.mvrx.args
|
||||||
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.platform.VectorBaseDialogFragment
|
||||||
|
import im.vector.app.databinding.FragmentSetLinkBinding
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import reactivecircus.flowbinding.android.widget.textChanges
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class SetLinkFragment :
|
||||||
|
VectorBaseDialogFragment<FragmentSetLinkBinding>() {
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Args(
|
||||||
|
val isTextSupported: Boolean,
|
||||||
|
val initialLink: String?,
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
private val viewModel: SetLinkViewModel by fragmentViewModel()
|
||||||
|
private val sharedActionViewModel: SetLinkSharedActionViewModel by viewModels(
|
||||||
|
ownerProducer = { requireParentFragment() }
|
||||||
|
)
|
||||||
|
private val args: Args by args()
|
||||||
|
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSetLinkBinding {
|
||||||
|
return FragmentSetLinkBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun show(isTextSupported: Boolean, initialLink: String?, fragmentManager: FragmentManager) =
|
||||||
|
SetLinkFragment().apply {
|
||||||
|
setArguments(Args(isTextSupported, initialLink))
|
||||||
|
}.show(fragmentManager, "SetLinkBottomSheet")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
views.link.setText(args.initialLink)
|
||||||
|
views.link.textChanges()
|
||||||
|
.onEach {
|
||||||
|
viewModel.handle(SetLinkAction.LinkChanged(it.toString()))
|
||||||
|
}
|
||||||
|
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
|
|
||||||
|
views.save.debouncedClicks {
|
||||||
|
viewModel.handle(
|
||||||
|
SetLinkAction.Save(
|
||||||
|
link = views.link.text.toString(),
|
||||||
|
text = views.text.text.toString(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
views.cancel.debouncedClicks(::onCancel)
|
||||||
|
views.remove.debouncedClicks(::onRemove)
|
||||||
|
|
||||||
|
viewModel.observeViewEvents {
|
||||||
|
when (it) {
|
||||||
|
is SetLinkViewEvents.SavedLinkAndText -> handleInsert(link = it.link, text = it.text)
|
||||||
|
is SetLinkViewEvents.SavedLink -> handleSet(link = it.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
views.toolbar.setNavigationOnClickListener {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) { viewState ->
|
||||||
|
views.toolbar.title = getString(
|
||||||
|
if (viewState.initialLink != null) {
|
||||||
|
R.string.set_link_edit
|
||||||
|
} else {
|
||||||
|
R.string.set_link_create
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
views.remove.isGone = !viewState.removeVisible
|
||||||
|
views.save.isEnabled = viewState.saveEnabled
|
||||||
|
views.textLayout.isGone = !viewState.isTextSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInsert(link: String, text: String) {
|
||||||
|
sharedActionViewModel.post(SetLinkSharedAction.Insert(text, link))
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSet(link: String) {
|
||||||
|
sharedActionViewModel.post(SetLinkSharedAction.Set(link))
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRemove() {
|
||||||
|
sharedActionViewModel.post(SetLinkSharedAction.Remove)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCancel() = dismiss()
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.link
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorSharedAction
|
||||||
|
import im.vector.app.core.platform.VectorSharedActionViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class SetLinkSharedActionViewModel @Inject constructor() :
|
||||||
|
VectorSharedActionViewModel<SetLinkSharedAction>()
|
||||||
|
|
||||||
|
sealed interface SetLinkSharedAction : VectorSharedAction {
|
||||||
|
data class Set(
|
||||||
|
val link: String,
|
||||||
|
) : SetLinkSharedAction
|
||||||
|
|
||||||
|
data class Insert(
|
||||||
|
val text: String,
|
||||||
|
val link: String,
|
||||||
|
) : SetLinkSharedAction
|
||||||
|
|
||||||
|
object Remove : SetLinkSharedAction
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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.link
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
|
||||||
|
sealed class SetLinkViewEvents : VectorViewEvents {
|
||||||
|
|
||||||
|
data class SavedLink(
|
||||||
|
val link: String,
|
||||||
|
) : SetLinkViewEvents()
|
||||||
|
|
||||||
|
data class SavedLinkAndText(
|
||||||
|
val link: String,
|
||||||
|
val text: String,
|
||||||
|
) : SetLinkViewEvents()
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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.link
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
|
||||||
|
class SetLinkViewModel @AssistedInject constructor(
|
||||||
|
@Assisted private val initialState: SetLinkViewState,
|
||||||
|
) : VectorViewModel<SetLinkViewState, SetLinkAction, SetLinkViewEvents>(initialState) {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory : MavericksAssistedViewModelFactory<SetLinkViewModel, SetLinkViewState> {
|
||||||
|
override fun create(initialState: SetLinkViewState): SetLinkViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MavericksViewModelFactory<SetLinkViewModel, SetLinkViewState> by hiltMavericksViewModelFactory()
|
||||||
|
|
||||||
|
override fun handle(action: SetLinkAction) = when (action) {
|
||||||
|
is SetLinkAction.LinkChanged -> handleLinkChanged(action.newLink)
|
||||||
|
is SetLinkAction.Save -> handleSave(action.link, action.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleLinkChanged(newLink: String) = setState {
|
||||||
|
copy(saveEnabled = newLink != initialLink.orEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSave(
|
||||||
|
link: String,
|
||||||
|
text: String
|
||||||
|
) = if (initialState.isTextSupported) {
|
||||||
|
_viewEvents.post(SetLinkViewEvents.SavedLinkAndText(link, text))
|
||||||
|
} else {
|
||||||
|
_viewEvents.post(SetLinkViewEvents.SavedLink(link))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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.link
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
|
||||||
|
data class SetLinkViewState(
|
||||||
|
val isTextSupported: Boolean,
|
||||||
|
val initialLink: String?,
|
||||||
|
val saveEnabled: Boolean,
|
||||||
|
) : MavericksState {
|
||||||
|
|
||||||
|
constructor(args: SetLinkFragment.Args) : this(
|
||||||
|
isTextSupported = args.isTextSupported,
|
||||||
|
initialLink = args.initialLink,
|
||||||
|
saveEnabled = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val removeVisible = initialLink != null
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ import android.graphics.drawable.Drawable
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.StyleRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
@ -113,19 +114,16 @@ object ThemeUtils {
|
||||||
*/
|
*/
|
||||||
fun setApplicationTheme(context: Context, aTheme: String) {
|
fun setApplicationTheme(context: Context, aTheme: String) {
|
||||||
currentTheme.set(aTheme)
|
currentTheme.set(aTheme)
|
||||||
context.setTheme(
|
context.setTheme(themeToRes(context, aTheme))
|
||||||
when (aTheme) {
|
|
||||||
SYSTEM_THEME_VALUE -> if (isSystemDarkTheme(context.resources)) R.style.Theme_Vector_Dark else R.style.Theme_Vector_Light
|
|
||||||
THEME_DARK_VALUE -> R.style.Theme_Vector_Dark
|
|
||||||
THEME_BLACK_VALUE -> R.style.Theme_Vector_Black
|
|
||||||
else -> R.style.Theme_Vector_Light
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Clear the cache
|
// Clear the cache
|
||||||
mColorByAttr.clear()
|
mColorByAttr.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@StyleRes
|
||||||
|
fun getApplicationThemeRes(context: Context) =
|
||||||
|
themeToRes(context, currentTheme.get())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the activity theme according to the selected one. Default is Light, so if this is the current
|
* Set the activity theme according to the selected one. Default is Light, so if this is the current
|
||||||
* theme, the theme is not changed.
|
* theme, the theme is not changed.
|
||||||
|
@ -200,4 +198,13 @@ object ThemeUtils {
|
||||||
DrawableCompat.setTint(tinted, color)
|
DrawableCompat.setTint(tinted, color)
|
||||||
return tinted
|
return tinted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@StyleRes
|
||||||
|
private fun themeToRes(context: Context, theme: String): Int =
|
||||||
|
when (theme) {
|
||||||
|
SYSTEM_THEME_VALUE -> if (isSystemDarkTheme(context.resources)) R.style.Theme_Vector_Dark else R.style.Theme_Vector_Light
|
||||||
|
THEME_DARK_VALUE -> R.style.Theme_Vector_Dark
|
||||||
|
THEME_BLACK_VALUE -> R.style.Theme_Vector_Black
|
||||||
|
else -> R.style.Theme_Vector_Light
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="44dp"
|
||||||
|
android:height="44dp"
|
||||||
|
android:viewportWidth="44"
|
||||||
|
android:viewportHeight="44">
|
||||||
|
<path
|
||||||
|
android:pathData="M22.566,16.151L23.101,15.616C24.577,14.14 26.956,14.126 28.415,15.585C29.874,17.044 29.86,19.423 28.383,20.899L25.844,23.438C24.368,24.915 21.989,24.929 20.53,23.47M21.434,27.849L20.899,28.383C19.423,29.86 17.044,29.874 15.585,28.415C14.126,26.956 14.14,24.577 15.616,23.101L18.156,20.562C19.632,19.086 22.011,19.071 23.47,20.53"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#8D97A5"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,117 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appBarLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?actionBarSize"
|
||||||
|
app:navigationIcon="@drawable/ic_x_18dp"
|
||||||
|
app:title="@string/set_link_create" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/save"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHeight_min="100dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/appBarLayout">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/textLayout"
|
||||||
|
style="@style/Widget.Vector.TextInputLayout.Form"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:hint="@string/set_link_text">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/linkLayout"
|
||||||
|
style="@style/Widget.Vector.TextInputLayout.Form"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:hint="@string/set_link_link">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/link"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textUri" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/save"
|
||||||
|
style="@style/Widget.Vector.Button.CallToAction"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:height="56dp"
|
||||||
|
android:text="@string/action_save"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/remove"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/remove"
|
||||||
|
style="@style/Widget.Vector.Button.Destructive"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:height="56dp"
|
||||||
|
android:text="@string/action_remove"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/cancel"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/cancel"
|
||||||
|
style="@style/Widget.Vector.Button.Outlined"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:height="56dp"
|
||||||
|
android:text="@string/action_cancel"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,157 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.link
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.test.MavericksTestRule
|
||||||
|
import im.vector.app.test.test
|
||||||
|
import im.vector.app.test.testDispatcher
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SetLinkViewModelTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val link = "https://matrix.org"
|
||||||
|
const val newLink = "https://matrix.org/new"
|
||||||
|
const val text = "Matrix"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val fragmentArgs = SetLinkFragment.Args(
|
||||||
|
isTextSupported = true,
|
||||||
|
initialLink = link
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createViewModel(
|
||||||
|
args: SetLinkFragment.Args
|
||||||
|
) = SetLinkViewModel(
|
||||||
|
initialState = SetLinkViewState(args),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no initial link, then remove button is hidden`() {
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
fragmentArgs
|
||||||
|
.copy(initialLink = null)
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
|
||||||
|
viewModelTest
|
||||||
|
.assertLatestState { !it.removeVisible }
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no initial link, when link changed, then remove button is still hidden`() {
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
fragmentArgs.copy(initialLink = null)
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(SetLinkAction.LinkChanged(newLink))
|
||||||
|
|
||||||
|
viewModelTest
|
||||||
|
.assertLatestState { !it.removeVisible }
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when link is unchanged, it disables the save button`() {
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
fragmentArgs
|
||||||
|
.copy(initialLink = link)
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
|
||||||
|
viewModelTest
|
||||||
|
.assertLatestState { !it.saveEnabled }
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when link is changed, it enables the save button`() {
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
fragmentArgs.copy(initialLink = link)
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(SetLinkAction.LinkChanged(newLink))
|
||||||
|
|
||||||
|
viewModelTest
|
||||||
|
.assertLatestState { it.saveEnabled }
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no initial link, when link is changed to empty, it disables the save button`() {
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
fragmentArgs.copy(initialLink = null)
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(SetLinkAction.LinkChanged(""))
|
||||||
|
|
||||||
|
viewModelTest
|
||||||
|
.assertLatestState {
|
||||||
|
!it.saveEnabled
|
||||||
|
}
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given text is supported, when saved, it emits the right event`() {
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
fragmentArgs.copy(isTextSupported = true)
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(
|
||||||
|
SetLinkAction.Save(link = newLink, text = text)
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModelTest
|
||||||
|
.assertEvent {
|
||||||
|
it == SetLinkViewEvents.SavedLinkAndText(
|
||||||
|
link = newLink,
|
||||||
|
text = text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given text is not supported, when saved, it emits the right event`() {
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
fragmentArgs.copy(isTextSupported = false)
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(
|
||||||
|
SetLinkAction.Save(link = newLink, text = text)
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModelTest
|
||||||
|
.assertEvent {
|
||||||
|
it == SetLinkViewEvents.SavedLink(link = newLink)
|
||||||
|
}
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue