diff --git a/changelog.d/7746.feature b/changelog.d/7746.feature
new file mode 100644
index 0000000000..6732d50b9c
--- /dev/null
+++ b/changelog.d/7746.feature
@@ -0,0 +1 @@
+[Rich text editor] Add support for links
diff --git a/dependencies.gradle b/dependencies.gradle
index dd8e6bb11c..75cd30ca2e 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -98,7 +98,7 @@ ext.libs = [
],
element : [
'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 : [
'moshi' : "com.squareup.moshi:moshi:$moshi",
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 4d0727e4c3..73cb60bb68 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3479,13 +3479,19 @@
Confirm
Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.
-
+
Apply bold format
Apply italic format
Apply strikethrough format
Apply underline format
+ Set link
Toggle full screen mode
+ Text
+ Link
+ Create a link
+ Edit link
+
In reply to
sent a file.
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index b58d584dad..d22ab51e7a 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -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.detail.TimelineViewModel
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.timeline.action.MessageActionsViewModel
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
@@ -691,4 +692,9 @@ interface MavericksViewModelModule {
fun vectorSettingsNotificationPreferenceViewModelFactory(
factory: VectorSettingsNotificationPreferenceViewModel.Factory
): MavericksAssistedViewModelFactory<*, *>
+
+ @Binds
+ @IntoMap
+ @MavericksViewModelKey(SetLinkViewModel::class)
+ fun setLinkViewModelFactory(factory: SetLinkViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt
new file mode 100644
index 0000000000..5a817b989e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt
@@ -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 : 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 VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
+ viewEvents
+ .stream()
+ .onEach {
+ observer(it)
+ }
+ .launchIn(viewLifecycleOwner.lifecycleScope)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
index d56ea8b733..4849e20b6d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
@@ -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.VoiceBroadcastAction
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.timeline.action.MessageSharedActionViewModel
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
@@ -147,6 +150,7 @@ class MessageComposerFragment : VectorBaseFragment(), A
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
+ private val setLinkActionsViewModel: SetLinkSharedActionViewModel by viewModels()
private val composer: MessageComposerView get() {
return if (vectorPreferences.isRichTextEditorEnabled()) {
@@ -212,6 +216,14 @@ class MessageComposerFragment : VectorBaseFragment(), A
.onEach { onTypeSelected(it.attachmentType) }
.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 }
.distinctUntilChanged()
.onEach { isFullScreen ->
@@ -385,6 +397,10 @@ class MessageComposerFragment : VectorBaseFragment(), A
override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state ->
messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(!state.isFullScreen))
}
+
+ override fun onSetLink(isTextSupported: Boolean, initialLink: String?) {
+ SetLinkFragment.show(isTextSupported, initialLink, childFragmentManager)
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt
index 44fcf22d4a..b68f4046c8 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt
@@ -45,4 +45,5 @@ interface Callback : ComposerEditText.Callback {
fun onAddAttachment()
fun onExpandOrCompactChange()
fun onFullScreenModeChanged()
+ fun onSetLink(isTextSupported: Boolean, initialLink: String?)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
index d69fe8edeb..543210e006 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
@@ -49,6 +49,7 @@ import im.vector.app.databinding.ComposerRichTextLayoutBinding
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
import io.element.android.wysiwyg.EditorEditText
import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
+import io.element.android.wysiwyg.inputhandlers.models.LinkAction
import io.element.android.wysiwyg.utils.RustErrorCollector
import uniffi.wysiwyg_composer.ActionState
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) {
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")
private fun disallowParentInterceptTouchEvent(view: View) {
view.setOnTouchListener { v, event ->
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkAction.kt
new file mode 100644
index 0000000000..5cc31022ea
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkAction.kt
@@ -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()
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkFragment.kt
new file mode 100644
index 0000000000..008a8017ee
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkFragment.kt
@@ -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() {
+
+ @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()
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkSharedActionViewModel.kt
new file mode 100644
index 0000000000..fb9f3f0d5b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkSharedActionViewModel.kt
@@ -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()
+
+sealed interface SetLinkSharedAction : VectorSharedAction {
+ data class Set(
+ val link: String,
+ ) : SetLinkSharedAction
+
+ data class Insert(
+ val text: String,
+ val link: String,
+ ) : SetLinkSharedAction
+
+ object Remove : SetLinkSharedAction
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewEvents.kt
new file mode 100644
index 0000000000..cd42651c22
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewEvents.kt
@@ -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()
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModel.kt
new file mode 100644
index 0000000000..9a5b5cd8dd
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModel.kt
@@ -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(initialState) {
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: SetLinkViewState): SetLinkViewModel
+ }
+
+ companion object : MavericksViewModelFactory 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))
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewState.kt
new file mode 100644
index 0000000000..ea61f7eb72
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewState.kt
@@ -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
+}
diff --git a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt
index b5c7b162d8..3c902d162e 100644
--- a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt
+++ b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt
@@ -24,6 +24,7 @@ import android.graphics.drawable.Drawable
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
+import androidx.annotation.StyleRes
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.graphics.drawable.DrawableCompat
@@ -113,19 +114,16 @@ object ThemeUtils {
*/
fun setApplicationTheme(context: Context, aTheme: String) {
currentTheme.set(aTheme)
- context.setTheme(
- 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
- }
- )
+ context.setTheme(themeToRes(context, aTheme))
// Clear the cache
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
* theme, the theme is not changed.
@@ -200,4 +198,13 @@ object ThemeUtils {
DrawableCompat.setTint(tinted, color)
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
+ }
}
diff --git a/vector/src/main/res/drawable/ic_composer_link.xml b/vector/src/main/res/drawable/ic_composer_link.xml
new file mode 100644
index 0000000000..6d0f731ed9
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_composer_link.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/vector/src/main/res/layout/fragment_set_link.xml b/vector/src/main/res/layout/fragment_set_link.xml
new file mode 100644
index 0000000000..36b3421253
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_set_link.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModelTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModelTest.kt
new file mode 100644
index 0000000000..9739fbec13
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModelTest.kt
@@ -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()
+ }
+}