From 50466792c627e89b7cc43e6e7b9644663ab3b9e5 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Wed, 21 Dec 2022 10:40:19 +0000 Subject: [PATCH] [Rich text editor] Add support for links in the rich text editor (#7746) --- changelog.d/7746.feature | 1 + dependencies.gradle | 2 +- .../src/main/res/values/strings.xml | 8 +- .../app/core/di/MavericksViewModelModule.kt | 6 + .../core/platform/VectorBaseDialogFragment.kt | 155 +++++++++++++++++ .../composer/MessageComposerFragment.kt | 16 ++ .../detail/composer/MessageComposerView.kt | 1 + .../detail/composer/RichTextComposerLayout.kt | 18 ++ .../detail/composer/link/SetLinkAction.kt | 30 ++++ .../detail/composer/link/SetLinkFragment.kt | 131 +++++++++++++++ .../link/SetLinkSharedActionViewModel.kt | 37 +++++ .../detail/composer/link/SetLinkViewEvents.kt | 31 ++++ .../detail/composer/link/SetLinkViewModel.kt | 55 ++++++ .../detail/composer/link/SetLinkViewState.kt | 34 ++++ .../vector/app/features/themes/ThemeUtils.kt | 23 ++- .../main/res/drawable/ic_composer_link.xml | 12 ++ .../src/main/res/layout/fragment_set_link.xml | 117 +++++++++++++ .../composer/link/SetLinkViewModelTest.kt | 157 ++++++++++++++++++ 18 files changed, 824 insertions(+), 10 deletions(-) create mode 100644 changelog.d/7746.feature create mode 100644 vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkSharedActionViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewEvents.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewState.kt create mode 100644 vector/src/main/res/drawable/ic_composer_link.xml create mode 100644 vector/src/main/res/layout/fragment_set_link.xml create mode 100644 vector/src/test/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModelTest.kt 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +