mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-02 12:16:55 +01:00
[Rich text editor] Add plain text mode and new attachment UI (#7459)
* Add new attachments selection dialog * Add rounded corners to bottom sheet dialog. Note these are currently only visible in the collapsed state. - [Google issue](https://issuetracker.google.com/issues/144859239) - [Rejected PR](https://github.com/material-components/material-components-android/pull/437) - [Github issue](https://github.com/material-components/material-components-android/issues/1278) * Add changelog entry * Remove redundant call to superclass click listener * Refactor to use view visibility helper * Change redundant sealed class to interface * Remove unused string * Revert "Add rounded corners to bottom sheet dialog." This reverts commit 17c43c91888162d3c7675511ff910c46c3aa32fc. * Remove redundant view group * Remove redundant `this` * Update rich text editor to latest * Update rich text editor version * Allow toggling rich text in the new editor * Persist the text formatting setting * Add changelog entry
This commit is contained in:
parent
bdfc96ff66
commit
c776aae9d0
1
changelog.d/7429.feature
Normal file
1
changelog.d/7429.feature
Normal file
@ -0,0 +1 @@
|
||||
Add new UI for selecting an attachment
|
1
changelog.d/7452.feature
Normal file
1
changelog.d/7452.feature
Normal file
@ -0,0 +1 @@
|
||||
[Rich text editor] Add plain text mode
|
@ -101,7 +101,7 @@ ext.libs = [
|
||||
],
|
||||
element : [
|
||||
'opusencoder' : "io.element.android:opusencoder:1.1.0",
|
||||
'wysiwyg' : "io.element.android:wysiwyg:0.2.1"
|
||||
'wysiwyg' : "io.element.android:wysiwyg:0.4.0"
|
||||
],
|
||||
squareup : [
|
||||
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
||||
|
@ -3205,6 +3205,16 @@
|
||||
<string name="tooltip_attachment_location">Share location</string>
|
||||
<string name="tooltip_attachment_voice_broadcast">Start a voice broadcast</string>
|
||||
|
||||
<string name="attachment_type_selector_gallery">Photo library</string>
|
||||
<string name="attachment_type_selector_sticker">Stickers</string>
|
||||
<string name="attachment_type_selector_file">Attachments</string>
|
||||
<string name="attachment_type_selector_voice_broadcast">Voice broadcast</string>
|
||||
<string name="attachment_type_selector_poll">Polls</string>
|
||||
<string name="attachment_type_selector_location">Location</string>
|
||||
<string name="attachment_type_selector_camera">Camera</string>
|
||||
<string name="attachment_type_selector_contact">Contact</string>
|
||||
<string name="attachment_type_selector_text_formatting">Text formatting</string>
|
||||
|
||||
<string name="message_reaction_show_less">Show less</string>
|
||||
<plurals name="message_reaction_show_more">
|
||||
<item quantity="one">"%1$d more"</item>
|
||||
|
@ -22,6 +22,7 @@ import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.IntoMap
|
||||
import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel
|
||||
import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel
|
||||
import im.vector.app.features.attachments.AttachmentTypeSelectorViewModel
|
||||
import im.vector.app.features.auth.ReAuthViewModel
|
||||
import im.vector.app.features.call.VectorCallViewModel
|
||||
import im.vector.app.features.call.conference.JitsiCallViewModel
|
||||
@ -677,4 +678,9 @@ interface MavericksViewModelModule {
|
||||
@IntoMap
|
||||
@MavericksViewModelKey(VectorSettingsLabsViewModel::class)
|
||||
fun vectorSettingsLabsViewModelFactory(factory: VectorSettingsLabsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@MavericksViewModelKey(AttachmentTypeSelectorViewModel::class)
|
||||
fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||
}
|
||||
|
@ -38,6 +38,10 @@ class BottomSheetActionButton @JvmOverloads constructor(
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
val views: ViewBottomSheetActionButtonBinding
|
||||
|
||||
override fun setOnClickListener(l: OnClickListener?) {
|
||||
views.bottomSheetActionClickableZone.setOnClickListener(l)
|
||||
}
|
||||
|
||||
var title: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
|
@ -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.attachments
|
||||
|
||||
import im.vector.app.core.utils.PERMISSIONS_EMPTY
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_BROADCAST
|
||||
|
||||
/**
|
||||
* The all possible types to pick with their required permissions.
|
||||
*/
|
||||
enum class AttachmentType(val permissions: List<String>) {
|
||||
CAMERA(PERMISSIONS_FOR_TAKING_PHOTO),
|
||||
GALLERY(PERMISSIONS_EMPTY),
|
||||
FILE(PERMISSIONS_EMPTY),
|
||||
STICKER(PERMISSIONS_EMPTY),
|
||||
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT),
|
||||
POLL(PERMISSIONS_EMPTY),
|
||||
LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING),
|
||||
VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST),
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.attachments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.databinding.BottomSheetAttachmentTypeSelectorBinding
|
||||
import im.vector.app.features.home.room.detail.TimelineViewModel
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetAttachmentTypeSelectorBinding>() {
|
||||
|
||||
private val viewModel: AttachmentTypeSelectorViewModel by parentFragmentViewModel()
|
||||
private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
|
||||
private val sharedActionViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels(
|
||||
ownerProducer = { requireParentFragment() }
|
||||
)
|
||||
|
||||
override val showExpanded = true
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetAttachmentTypeSelectorBinding {
|
||||
return BottomSheetAttachmentTypeSelectorBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel, timelineViewModel) { viewState, timelineState ->
|
||||
super.invalidate()
|
||||
views.location.isVisible = viewState.isLocationVisible
|
||||
views.voiceBroadcast.isVisible = viewState.isVoiceBroadcastVisible
|
||||
views.poll.isVisible = !timelineState.isThreadTimeline()
|
||||
views.textFormatting.isChecked = viewState.isTextFormattingEnabled
|
||||
views.textFormatting.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
if (viewState.isTextFormattingEnabled) {
|
||||
R.drawable.ic_text_formatting
|
||||
} else {
|
||||
R.drawable.ic_text_formatting_disabled
|
||||
}, 0, 0, 0
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
views.gallery.debouncedClicks { onAttachmentSelected(AttachmentType.GALLERY) }
|
||||
views.stickers.debouncedClicks { onAttachmentSelected(AttachmentType.STICKER) }
|
||||
views.file.debouncedClicks { onAttachmentSelected(AttachmentType.FILE) }
|
||||
views.voiceBroadcast.debouncedClicks { onAttachmentSelected(AttachmentType.VOICE_BROADCAST) }
|
||||
views.poll.debouncedClicks { onAttachmentSelected(AttachmentType.POLL) }
|
||||
views.location.debouncedClicks { onAttachmentSelected(AttachmentType.LOCATION) }
|
||||
views.camera.debouncedClicks { onAttachmentSelected(AttachmentType.CAMERA) }
|
||||
views.contact.debouncedClicks { onAttachmentSelected(AttachmentType.CONTACT) }
|
||||
views.textFormatting.setOnCheckedChangeListener { _, isChecked -> onTextFormattingToggled(isChecked) }
|
||||
}
|
||||
|
||||
private fun onAttachmentSelected(attachmentType: AttachmentType) {
|
||||
val action = AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction(attachmentType)
|
||||
sharedActionViewModel.post(action)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun onTextFormattingToggled(isEnabled: Boolean) =
|
||||
viewModel.handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled))
|
||||
|
||||
companion object {
|
||||
fun show(fragmentManager: FragmentManager) {
|
||||
val bottomSheet = AttachmentTypeSelectorBottomSheet()
|
||||
bottomSheet.show(fragmentManager, "AttachmentTypeSelectorBottomSheet")
|
||||
}
|
||||
}
|
||||
}
|
@ -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.attachments
|
||||
|
||||
import im.vector.app.core.platform.VectorSharedAction
|
||||
import im.vector.app.core.platform.VectorSharedActionViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
class AttachmentTypeSelectorSharedActionViewModel @Inject constructor() :
|
||||
VectorSharedActionViewModel<AttachmentTypeSelectorSharedAction>()
|
||||
|
||||
sealed interface AttachmentTypeSelectorSharedAction : VectorSharedAction {
|
||||
data class SelectAttachmentTypeAction(
|
||||
val attachmentType: AttachmentType
|
||||
) : AttachmentTypeSelectorSharedAction
|
||||
}
|
@ -30,17 +30,11 @@ import android.view.animation.TranslateAnimation
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.PopupWindow
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.TooltipCompat
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.utils.PERMISSIONS_EMPTY
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_BROADCAST
|
||||
import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding
|
||||
import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback
|
||||
import kotlin.math.max
|
||||
@ -59,7 +53,7 @@ class AttachmentTypeSelectorView(
|
||||
) : PopupWindow(context) {
|
||||
|
||||
interface Callback {
|
||||
fun onTypeSelected(type: Type)
|
||||
fun onTypeSelected(type: AttachmentType)
|
||||
}
|
||||
|
||||
private val views: ViewAttachmentTypeSelectorBinding
|
||||
@ -69,14 +63,14 @@ class AttachmentTypeSelectorView(
|
||||
init {
|
||||
contentView = inflater.inflate(R.layout.view_attachment_type_selector, null, false)
|
||||
views = ViewAttachmentTypeSelectorBinding.bind(contentView)
|
||||
views.attachmentGalleryButton.configure(Type.GALLERY)
|
||||
views.attachmentCameraButton.configure(Type.CAMERA)
|
||||
views.attachmentFileButton.configure(Type.FILE)
|
||||
views.attachmentStickersButton.configure(Type.STICKER)
|
||||
views.attachmentContactButton.configure(Type.CONTACT)
|
||||
views.attachmentPollButton.configure(Type.POLL)
|
||||
views.attachmentLocationButton.configure(Type.LOCATION)
|
||||
views.attachmentVoiceBroadcast.configure(Type.VOICE_BROADCAST)
|
||||
views.attachmentGalleryButton.configure(AttachmentType.GALLERY)
|
||||
views.attachmentCameraButton.configure(AttachmentType.CAMERA)
|
||||
views.attachmentFileButton.configure(AttachmentType.FILE)
|
||||
views.attachmentStickersButton.configure(AttachmentType.STICKER)
|
||||
views.attachmentContactButton.configure(AttachmentType.CONTACT)
|
||||
views.attachmentPollButton.configure(AttachmentType.POLL)
|
||||
views.attachmentLocationButton.configure(AttachmentType.LOCATION)
|
||||
views.attachmentVoiceBroadcast.configure(AttachmentType.VOICE_BROADCAST)
|
||||
width = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
height = LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
animationStyle = 0
|
||||
@ -127,16 +121,16 @@ class AttachmentTypeSelectorView(
|
||||
}
|
||||
}
|
||||
|
||||
fun setAttachmentVisibility(type: Type, isVisible: Boolean) {
|
||||
fun setAttachmentVisibility(type: AttachmentType, isVisible: Boolean) {
|
||||
when (type) {
|
||||
Type.CAMERA -> views.attachmentCameraButton
|
||||
Type.GALLERY -> views.attachmentGalleryButton
|
||||
Type.FILE -> views.attachmentFileButton
|
||||
Type.STICKER -> views.attachmentStickersButton
|
||||
Type.CONTACT -> views.attachmentContactButton
|
||||
Type.POLL -> views.attachmentPollButton
|
||||
Type.LOCATION -> views.attachmentLocationButton
|
||||
Type.VOICE_BROADCAST -> views.attachmentVoiceBroadcast
|
||||
AttachmentType.CAMERA -> views.attachmentCameraButton
|
||||
AttachmentType.GALLERY -> views.attachmentGalleryButton
|
||||
AttachmentType.FILE -> views.attachmentFileButton
|
||||
AttachmentType.STICKER -> views.attachmentStickersButton
|
||||
AttachmentType.CONTACT -> views.attachmentContactButton
|
||||
AttachmentType.POLL -> views.attachmentPollButton
|
||||
AttachmentType.LOCATION -> views.attachmentLocationButton
|
||||
AttachmentType.VOICE_BROADCAST -> views.attachmentVoiceBroadcast
|
||||
}.let {
|
||||
it.isVisible = isVisible
|
||||
}
|
||||
@ -200,13 +194,13 @@ class AttachmentTypeSelectorView(
|
||||
return Pair(x, y)
|
||||
}
|
||||
|
||||
private fun ImageButton.configure(type: Type): ImageButton {
|
||||
private fun ImageButton.configure(type: AttachmentType): ImageButton {
|
||||
this.setOnClickListener(TypeClickListener(type))
|
||||
TooltipCompat.setTooltipText(this, context.getString(type.tooltipRes))
|
||||
TooltipCompat.setTooltipText(this, context.getString(attachmentTooltipLabels.getValue(type)))
|
||||
return this
|
||||
}
|
||||
|
||||
private inner class TypeClickListener(private val type: Type) : View.OnClickListener {
|
||||
private inner class TypeClickListener(private val type: AttachmentType) : View.OnClickListener {
|
||||
|
||||
override fun onClick(v: View) {
|
||||
dismiss()
|
||||
@ -217,14 +211,18 @@ class AttachmentTypeSelectorView(
|
||||
/**
|
||||
* The all possible types to pick with their required permissions and tooltip resource.
|
||||
*/
|
||||
enum class Type(val permissions: List<String>, @StringRes val tooltipRes: Int) {
|
||||
CAMERA(PERMISSIONS_FOR_TAKING_PHOTO, R.string.tooltip_attachment_photo),
|
||||
GALLERY(PERMISSIONS_EMPTY, R.string.tooltip_attachment_gallery),
|
||||
FILE(PERMISSIONS_EMPTY, R.string.tooltip_attachment_file),
|
||||
STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker),
|
||||
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact),
|
||||
POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll),
|
||||
LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, R.string.tooltip_attachment_location),
|
||||
VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST, R.string.tooltip_attachment_voice_broadcast),
|
||||
private companion object {
|
||||
private val attachmentTooltipLabels: Map<AttachmentType, Int> = AttachmentType.values().associateWith {
|
||||
when (it) {
|
||||
AttachmentType.CAMERA -> R.string.tooltip_attachment_photo
|
||||
AttachmentType.GALLERY -> R.string.tooltip_attachment_gallery
|
||||
AttachmentType.FILE -> R.string.tooltip_attachment_file
|
||||
AttachmentType.STICKER -> R.string.tooltip_attachment_sticker
|
||||
AttachmentType.CONTACT -> R.string.tooltip_attachment_contact
|
||||
AttachmentType.POLL -> R.string.tooltip_attachment_poll
|
||||
AttachmentType.LOCATION -> R.string.tooltip_attachment_location
|
||||
AttachmentType.VOICE_BROADCAST -> R.string.tooltip_attachment_voice_broadcast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.attachments
|
||||
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
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.EmptyViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import im.vector.app.features.VectorFeatures
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
|
||||
class AttachmentTypeSelectorViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: AttachmentTypeSelectorViewState,
|
||||
private val vectorFeatures: VectorFeatures,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
) : VectorViewModel<AttachmentTypeSelectorViewState, AttachmentTypeSelectorAction, EmptyViewEvents>(initialState) {
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<AttachmentTypeSelectorViewModel, AttachmentTypeSelectorViewState> {
|
||||
override fun create(initialState: AttachmentTypeSelectorViewState): AttachmentTypeSelectorViewModel
|
||||
}
|
||||
|
||||
companion object : MavericksViewModelFactory<AttachmentTypeSelectorViewModel, AttachmentTypeSelectorViewState> by hiltMavericksViewModelFactory()
|
||||
|
||||
override fun handle(action: AttachmentTypeSelectorAction) = when (action) {
|
||||
is AttachmentTypeSelectorAction.ToggleTextFormatting -> setTextFormattingEnabled(action.isEnabled)
|
||||
}
|
||||
|
||||
init {
|
||||
setState {
|
||||
copy(
|
||||
isLocationVisible = vectorFeatures.isLocationSharingEnabled(),
|
||||
isVoiceBroadcastVisible = vectorFeatures.isVoiceBroadcastEnabled(),
|
||||
isTextFormattingEnabled = vectorPreferences.isTextFormattingEnabled(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTextFormattingEnabled(isEnabled: Boolean) {
|
||||
vectorPreferences.setTextFormattingEnabled(isEnabled)
|
||||
setState {
|
||||
copy(
|
||||
isTextFormattingEnabled = isEnabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class AttachmentTypeSelectorViewState(
|
||||
val isLocationVisible: Boolean = false,
|
||||
val isVoiceBroadcastVisible: Boolean = false,
|
||||
val isTextFormattingEnabled: Boolean = false,
|
||||
) : MavericksState
|
||||
|
||||
sealed interface AttachmentTypeSelectorAction : VectorViewModelAction {
|
||||
data class ToggleTextFormatting(val isEnabled: Boolean) : AttachmentTypeSelectorAction
|
||||
}
|
@ -54,7 +54,7 @@ class AttachmentsHelper(
|
||||
private var captureUri: Uri? = null
|
||||
|
||||
// The pending type is set if we have to handle permission request. It must be restored if the activity gets killed.
|
||||
var pendingType: AttachmentTypeSelectorView.Type? = null
|
||||
var pendingType: AttachmentType? = null
|
||||
|
||||
// Restorable
|
||||
|
||||
|
@ -40,8 +40,10 @@ import androidx.core.text.buildSpannedString
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
@ -63,7 +65,12 @@ import im.vector.app.core.utils.onPermissionDeniedDialog
|
||||
import im.vector.app.core.utils.registerForPermissionsResult
|
||||
import im.vector.app.databinding.FragmentComposerBinding
|
||||
import im.vector.app.features.VectorFeatures
|
||||
import im.vector.app.features.attachments.AttachmentType
|
||||
import im.vector.app.features.attachments.AttachmentTypeSelectorBottomSheet
|
||||
import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction
|
||||
import im.vector.app.features.attachments.AttachmentTypeSelectorSharedActionViewModel
|
||||
import im.vector.app.features.attachments.AttachmentTypeSelectorView
|
||||
import im.vector.app.features.attachments.AttachmentTypeSelectorViewModel
|
||||
import im.vector.app.features.attachments.AttachmentsHelper
|
||||
import im.vector.app.features.attachments.ContactAttachment
|
||||
import im.vector.app.features.attachments.ShareIntentHandler
|
||||
@ -91,8 +98,9 @@ import im.vector.app.features.poll.PollMode
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.app.features.share.SharedData
|
||||
import im.vector.app.features.voice.VoiceFailure
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@ -162,6 +170,8 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
||||
private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
|
||||
private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel()
|
||||
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
|
||||
private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
|
||||
private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
|
||||
|
||||
private val composer: MessageComposerView get() {
|
||||
return if (vectorPreferences.isRichTextEditorEnabled()) {
|
||||
@ -227,6 +237,11 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
||||
}
|
||||
}
|
||||
|
||||
attachmentActionsViewModel.stream()
|
||||
.filterIsInstance<AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction>()
|
||||
.onEach { onTypeSelected(it.attachmentType) }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
handleShareData()
|
||||
}
|
||||
@ -260,11 +275,14 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
||||
messageComposerViewModel.endAllVoiceActions()
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState ->
|
||||
override fun invalidate() = withState(
|
||||
timelineViewModel, messageComposerViewModel, attachmentViewModel
|
||||
) { mainState, messageComposerState, attachmentState ->
|
||||
if (mainState.tombstoneEvent != null) return@withState
|
||||
|
||||
composer.setInvisible(!messageComposerState.isComposerVisible)
|
||||
composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
|
||||
(composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled
|
||||
}
|
||||
|
||||
private fun setupComposer() {
|
||||
@ -307,21 +325,25 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
||||
}
|
||||
composer.callback = object : Callback {
|
||||
override fun onAddAttachment() {
|
||||
if (!::attachmentTypeSelector.isInitialized) {
|
||||
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment)
|
||||
attachmentTypeSelector.setAttachmentVisibility(
|
||||
AttachmentTypeSelectorView.Type.LOCATION,
|
||||
vectorFeatures.isLocationSharingEnabled(),
|
||||
)
|
||||
attachmentTypeSelector.setAttachmentVisibility(
|
||||
AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine()
|
||||
)
|
||||
attachmentTypeSelector.setAttachmentVisibility(
|
||||
AttachmentTypeSelectorView.Type.VOICE_BROADCAST,
|
||||
vectorPreferences.isVoiceBroadcastEnabled(), // TODO check user permission
|
||||
)
|
||||
if (vectorPreferences.isRichTextEditorEnabled()) {
|
||||
AttachmentTypeSelectorBottomSheet.show(childFragmentManager)
|
||||
} else {
|
||||
if (!::attachmentTypeSelector.isInitialized) {
|
||||
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment)
|
||||
attachmentTypeSelector.setAttachmentVisibility(
|
||||
AttachmentType.LOCATION,
|
||||
vectorFeatures.isLocationSharingEnabled(),
|
||||
)
|
||||
attachmentTypeSelector.setAttachmentVisibility(
|
||||
AttachmentType.POLL, !isThreadTimeLine()
|
||||
)
|
||||
attachmentTypeSelector.setAttachmentVisibility(
|
||||
AttachmentType.VOICE_BROADCAST,
|
||||
vectorPreferences.isVoiceBroadcastEnabled(), // TODO check user permission
|
||||
)
|
||||
}
|
||||
attachmentTypeSelector.show(composer.attachmentButton)
|
||||
}
|
||||
attachmentTypeSelector.show(composer.attachmentButton)
|
||||
}
|
||||
|
||||
override fun onExpandOrCompactChange() {
|
||||
@ -678,20 +700,20 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
|
||||
private fun launchAttachmentProcess(type: AttachmentType) {
|
||||
when (type) {
|
||||
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(
|
||||
AttachmentType.CAMERA -> attachmentsHelper.openCamera(
|
||||
activity = requireActivity(),
|
||||
vectorPreferences = vectorPreferences,
|
||||
cameraActivityResultLauncher = attachmentCameraActivityResultLauncher,
|
||||
cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher
|
||||
)
|
||||
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
|
||||
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
|
||||
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
|
||||
AttachmentTypeSelectorView.Type.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment)
|
||||
AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomId, null, PollMode.CREATE)
|
||||
AttachmentTypeSelectorView.Type.LOCATION -> {
|
||||
AttachmentType.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
|
||||
AttachmentType.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
|
||||
AttachmentType.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
|
||||
AttachmentType.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment)
|
||||
AttachmentType.POLL -> navigator.openCreatePoll(requireContext(), roomId, null, PollMode.CREATE)
|
||||
AttachmentType.LOCATION -> {
|
||||
navigator
|
||||
.openLocationSharing(
|
||||
context = requireContext(),
|
||||
@ -701,11 +723,11 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
||||
locationOwnerId = session.myUserId
|
||||
)
|
||||
}
|
||||
AttachmentTypeSelectorView.Type.VOICE_BROADCAST -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Start)
|
||||
AttachmentType.VOICE_BROADCAST -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Start)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) {
|
||||
override fun onTypeSelected(type: AttachmentType) {
|
||||
if (checkPermissions(type.permissions, requireActivity(), typeSelectedActivityResultLauncher)) {
|
||||
launchAttachmentProcess(type)
|
||||
} else {
|
||||
|
@ -38,7 +38,7 @@ import im.vector.app.core.extensions.setTextIfDifferent
|
||||
import im.vector.app.databinding.ComposerRichTextLayoutBinding
|
||||
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
|
||||
import io.element.android.wysiwyg.EditorEditText
|
||||
import io.element.android.wysiwyg.InlineFormat
|
||||
import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
|
||||
import uniffi.wysiwyg_composer.ComposerAction
|
||||
import uniffi.wysiwyg_composer.MenuState
|
||||
|
||||
@ -57,12 +57,24 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
||||
|
||||
private var isFullScreen = false
|
||||
|
||||
var isTextFormattingEnabled = true
|
||||
set(value) {
|
||||
if (field == value) return
|
||||
syncEditTexts()
|
||||
field = value
|
||||
updateEditTextVisibility()
|
||||
}
|
||||
|
||||
override val text: Editable?
|
||||
get() = views.composerEditText.text
|
||||
get() = editText.text
|
||||
override val formattedText: String?
|
||||
get() = views.composerEditText.getHtmlOutput()
|
||||
get() = (editText as? EditorEditText)?.getHtmlOutput()
|
||||
override val editText: EditText
|
||||
get() = views.composerEditText
|
||||
get() = if (isTextFormattingEnabled) {
|
||||
views.richTextComposerEditText
|
||||
} else {
|
||||
views.plainTextComposerEditText
|
||||
}
|
||||
override val emojiButton: ImageButton?
|
||||
get() = null
|
||||
override val sendButton: ImageButton
|
||||
@ -91,21 +103,12 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
||||
|
||||
collapse(false)
|
||||
|
||||
views.composerEditText.addTextChangedListener(object : TextWatcher {
|
||||
private var previousTextWasExpanded = false
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
callback?.onTextChanged(s)
|
||||
|
||||
val isExpanded = s.lines().count() > 1
|
||||
if (previousTextWasExpanded != isExpanded) {
|
||||
updateTextFieldBorder(isExpanded)
|
||||
}
|
||||
previousTextWasExpanded = isExpanded
|
||||
}
|
||||
})
|
||||
views.richTextComposerEditText.addTextChangedListener(
|
||||
TextChangeListener({ callback?.onTextChanged(it) }, ::updateTextFieldBorder)
|
||||
)
|
||||
views.plainTextComposerEditText.addTextChangedListener(
|
||||
TextChangeListener({ callback?.onTextChanged(it) }, ::updateTextFieldBorder)
|
||||
)
|
||||
|
||||
views.composerRelatedMessageCloseButton.setOnClickListener {
|
||||
collapse()
|
||||
@ -130,19 +133,23 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
||||
|
||||
private fun setupRichTextMenu() {
|
||||
addRichTextMenuItem(R.drawable.ic_composer_bold, R.string.rich_text_editor_format_bold, ComposerAction.Bold) {
|
||||
views.composerEditText.toggleInlineFormat(InlineFormat.Bold)
|
||||
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Bold)
|
||||
}
|
||||
addRichTextMenuItem(R.drawable.ic_composer_italic, R.string.rich_text_editor_format_italic, ComposerAction.Italic) {
|
||||
views.composerEditText.toggleInlineFormat(InlineFormat.Italic)
|
||||
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Italic)
|
||||
}
|
||||
addRichTextMenuItem(R.drawable.ic_composer_underlined, R.string.rich_text_editor_format_underline, ComposerAction.Underline) {
|
||||
views.composerEditText.toggleInlineFormat(InlineFormat.Underline)
|
||||
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Underline)
|
||||
}
|
||||
addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.StrikeThrough) {
|
||||
views.composerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
|
||||
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
|
||||
}
|
||||
}
|
||||
|
||||
views.composerEditText.menuStateChangedListener = EditorEditText.OnMenuStateChangedListener { state ->
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
views.richTextComposerEditText.menuStateChangedListener = EditorEditText.OnMenuStateChangedListener { state ->
|
||||
if (state is MenuState.Update) {
|
||||
updateMenuStateFor(ComposerAction.Bold, state)
|
||||
updateMenuStateFor(ComposerAction.Italic, state)
|
||||
@ -150,8 +157,26 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
||||
updateMenuStateFor(ComposerAction.StrikeThrough, state)
|
||||
}
|
||||
}
|
||||
|
||||
updateEditTextVisibility()
|
||||
}
|
||||
|
||||
private fun updateEditTextVisibility() {
|
||||
views.richTextComposerEditText.isVisible = isTextFormattingEnabled
|
||||
views.richTextMenu.isVisible = isTextFormattingEnabled
|
||||
views.plainTextComposerEditText.isVisible = !isTextFormattingEnabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the non-active input with the contents of the active input.
|
||||
*/
|
||||
private fun syncEditTexts() =
|
||||
if (isTextFormattingEnabled) {
|
||||
views.plainTextComposerEditText.setText(views.richTextComposerEditText.getPlainText())
|
||||
} else {
|
||||
views.richTextComposerEditText.setText(views.plainTextComposerEditText.text.toString())
|
||||
}
|
||||
|
||||
private fun addRichTextMenuItem(@DrawableRes iconId: Int, @StringRes description: Int, action: ComposerAction, onClick: () -> Unit) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val button = ViewRichTextMenuButtonBinding.inflate(inflater, views.richTextMenu, true)
|
||||
@ -181,7 +206,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun replaceFormattedContent(text: CharSequence) {
|
||||
views.composerEditText.setHtml(text.toString())
|
||||
views.richTextComposerEditText.setHtml(text.toString())
|
||||
}
|
||||
|
||||
override fun collapse(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||
@ -191,6 +216,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
||||
}
|
||||
currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_compact
|
||||
applyNewConstraintSet(animate, transitionComplete)
|
||||
updateEditTextVisibility()
|
||||
}
|
||||
|
||||
override fun expand(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||
@ -200,10 +226,11 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
||||
}
|
||||
currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_expanded
|
||||
applyNewConstraintSet(animate, transitionComplete)
|
||||
updateEditTextVisibility()
|
||||
}
|
||||
|
||||
override fun setTextIfDifferent(text: CharSequence?): Boolean {
|
||||
return views.composerEditText.setTextIfDifferent(text)
|
||||
return editText.setTextIfDifferent(text)
|
||||
}
|
||||
|
||||
override fun toggleFullScreen(newValue: Boolean) {
|
||||
@ -214,6 +241,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
updateTextFieldBorder(newValue)
|
||||
updateEditTextVisibility()
|
||||
}
|
||||
|
||||
private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||
@ -233,4 +261,23 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
||||
override fun setInvisible(isInvisible: Boolean) {
|
||||
this.isInvisible = isInvisible
|
||||
}
|
||||
|
||||
private class TextChangeListener(
|
||||
private val onTextChanged: (s: Editable) -> Unit,
|
||||
private val onExpandedChanged: (isExpanded: Boolean) -> Unit,
|
||||
) : TextWatcher {
|
||||
private var previousTextWasExpanded = false
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
onTextChanged.invoke(s)
|
||||
|
||||
val isExpanded = s.lines().count() > 1
|
||||
if (previousTextWasExpanded != isExpanded) {
|
||||
onExpandedChanged(isExpanded)
|
||||
}
|
||||
previousTextWasExpanded = isExpanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,6 +109,7 @@ class VectorPreferences @Inject constructor(
|
||||
const val SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY"
|
||||
private const val SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY"
|
||||
private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY"
|
||||
private const val SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY = "SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY"
|
||||
private const val SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY"
|
||||
private const val SETTINGS_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_KEY"
|
||||
private const val SETTINGS_SHOW_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY"
|
||||
@ -759,6 +760,24 @@ class VectorPreferences @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if text formatting is enabled within the rich text editor.
|
||||
*
|
||||
* @return true if the text formatting is enabled
|
||||
*/
|
||||
fun isTextFormattingEnabled(): Boolean =
|
||||
defaultPrefs.getBoolean(SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY, true)
|
||||
|
||||
/**
|
||||
* Update whether text formatting is enabled within the rich text editor.
|
||||
*
|
||||
* @param isEnabled true to enable the text formatting
|
||||
*/
|
||||
fun setTextFormattingEnabled(isEnabled: Boolean) =
|
||||
defaultPrefs.edit {
|
||||
putBoolean(SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY, isEnabled)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if a confirmation dialog should be displayed before staring a call.
|
||||
*/
|
||||
|
13
vector/src/main/res/drawable/ic_text_formatting.xml
Normal file
13
vector/src/main/res/drawable/ic_text_formatting.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M3,20.667C3,21.4 3.6,22 4.333,22H20.333C21.067,22 21.667,21.4 21.667,20.667C21.667,19.933 21.067,19.333 20.333,19.333H4.333C3.6,19.333 3,19.933 3,20.667ZM9,13.733H15.667L16.547,15.867C16.747,16.347 17.213,16.667 17.733,16.667C18.653,16.667 19.267,15.72 18.907,14.88L13.733,2.92C13.493,2.36 12.947,2 12.333,2C11.72,2 11.173,2.36 10.933,2.92L5.76,14.88C5.4,15.72 6.027,16.667 6.947,16.667C7.467,16.667 7.933,16.347 8.133,15.867L9,13.733ZM12.333,4.64L14.827,11.333H9.84L12.333,4.64Z"
|
||||
android:fillColor="#0DBD8B"/>
|
||||
</group>
|
||||
</vector>
|
18
vector/src/main/res/drawable/ic_text_formatting_disabled.xml
Normal file
18
vector/src/main/res/drawable/ic_text_formatting_disabled.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M9,15.733H15.667L16.547,17.867C16.747,18.347 17.213,18.667 17.733,18.667C18.653,18.667 19.267,17.72 18.907,16.88L13.733,4.92C13.493,4.36 12.947,4 12.333,4C11.72,4 11.173,4.36 10.933,4.92L5.76,16.88C5.4,17.72 6.027,18.667 6.947,18.667C7.467,18.667 7.933,18.347 8.133,17.867L9,15.733ZM12.333,6.64L14.827,13.333H9.84L12.333,6.64Z"
|
||||
android:fillColor="#0DBD8B"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M2.5,11.667C2.5,12.676 3.324,13.5 4.333,13.5H20.333C21.343,13.5 22.167,12.676 22.167,11.667C22.167,10.657 21.343,9.833 20.333,9.833H4.333C3.324,9.833 2.5,10.657 2.5,11.667Z"
|
||||
android:fillColor="#0DBD8B"
|
||||
android:strokeColor="#ffffff"/>
|
||||
</group>
|
||||
</vector>
|
@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView 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"
|
||||
android:background="?colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/gallery"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/attachment_type_selector_gallery"
|
||||
app:leftIcon="@drawable/ic_attachment_gallery"
|
||||
app:tint="?colorPrimary"
|
||||
app:titleTextColor="?vctr_content_primary" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/stickers"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/attachment_type_selector_sticker"
|
||||
app:leftIcon="@drawable/ic_attachment_sticker"
|
||||
app:tint="?colorPrimary"
|
||||
app:titleTextColor="?vctr_content_primary" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/file"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/attachment_type_selector_file"
|
||||
app:leftIcon="@drawable/ic_attachment_file"
|
||||
app:tint="?colorPrimary"
|
||||
app:titleTextColor="?vctr_content_primary" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/voiceBroadcast"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/attachment_type_selector_voice_broadcast"
|
||||
app:leftIcon="@drawable/ic_attachment_voice_broadcast"
|
||||
app:tint="?colorPrimary"
|
||||
app:titleTextColor="?vctr_content_primary" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/poll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/attachment_type_selector_poll"
|
||||
app:leftIcon="@drawable/ic_attachment_poll"
|
||||
app:tint="?colorPrimary"
|
||||
app:titleTextColor="?vctr_content_primary" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/location"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/attachment_type_selector_location"
|
||||
app:leftIcon="@drawable/ic_attachment_location"
|
||||
app:tint="?colorPrimary"
|
||||
app:titleTextColor="?vctr_content_primary" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/camera"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/attachment_type_selector_camera"
|
||||
app:leftIcon="@drawable/ic_attachment_camera"
|
||||
app:tint="?colorPrimary"
|
||||
app:titleTextColor="?vctr_content_primary" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/contact"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/attachment_type_selector_contact"
|
||||
app:leftIcon="@drawable/ic_attachment_contact_white_24dp"
|
||||
app:tint="?colorPrimary"
|
||||
app:titleTextColor="?vctr_content_primary" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?vctr_list_separator" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/textFormatting"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableStart="@drawable/ic_text_formatting"
|
||||
android:drawablePadding="20dp"
|
||||
android:padding="20dp"
|
||||
android:paddingStart="28dp"
|
||||
android:text="@string/attachment_type_selector_text_formatting"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Subtitle"
|
||||
android:textColor="?vctr_content_primary"
|
||||
app:drawableTint="?colorPrimary"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
@ -104,13 +104,26 @@
|
||||
android:background="@drawable/bg_composer_rich_edit_text_single_line" />
|
||||
|
||||
<io.element.android.wysiwyg.EditorEditText
|
||||
android:id="@+id/composerEditText"
|
||||
android:id="@+id/richTextComposerEditText"
|
||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top"
|
||||
android:nextFocusLeft="@id/composerEditText"
|
||||
android:nextFocusUp="@id/composerEditText"
|
||||
android:nextFocusLeft="@id/richTextComposerEditText"
|
||||
android:nextFocusUp="@id/richTextComposerEditText"
|
||||
tools:hint="@string/room_message_placeholder"
|
||||
tools:text="@tools:sample/lorem/random"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<!-- Use a separate EditText for plain text editing while the rich text editor doesn't support this mode -->
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/plainTextComposerEditText"
|
||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top"
|
||||
android:nextFocusLeft="@id/plainTextComposerEditText"
|
||||
android:nextFocusUp="@id/plainTextComposerEditText"
|
||||
tools:hint="@string/room_message_placeholder"
|
||||
tools:text="@tools:sample/lorem/random"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
@ -136,13 +136,29 @@
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<io.element.android.wysiwyg.EditorEditText
|
||||
android:id="@+id/composerEditText"
|
||||
android:id="@+id/richTextComposerEditText"
|
||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/room_message_placeholder"
|
||||
android:nextFocusLeft="@id/composerEditText"
|
||||
android:nextFocusUp="@id/composerEditText"
|
||||
android:nextFocusLeft="@id/richTextComposerEditText"
|
||||
android:nextFocusUp="@id/richTextComposerEditText"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||
app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
|
||||
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
|
||||
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/plainTextComposerEditText"
|
||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/room_message_placeholder"
|
||||
android:nextFocusLeft="@id/plainTextComposerEditText"
|
||||
android:nextFocusUp="@id/plainTextComposerEditText"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||
|
@ -149,13 +149,29 @@
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<io.element.android.wysiwyg.EditorEditText
|
||||
android:id="@+id/composerEditText"
|
||||
android:id="@+id/richTextComposerEditText"
|
||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/room_message_placeholder"
|
||||
android:nextFocusLeft="@id/composerEditText"
|
||||
android:nextFocusUp="@id/composerEditText"
|
||||
android:nextFocusLeft="@id/richTextComposerEditText"
|
||||
android:nextFocusUp="@id/richTextComposerEditText"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||
app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
|
||||
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
|
||||
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/plainTextComposerEditText"
|
||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/room_message_placeholder"
|
||||
android:nextFocusLeft="@id/plainTextComposerEditText"
|
||||
android:nextFocusUp="@id/plainTextComposerEditText"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||
|
@ -136,13 +136,30 @@
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<io.element.android.wysiwyg.EditorEditText
|
||||
android:id="@+id/composerEditText"
|
||||
android:id="@+id/richTextComposerEditText"
|
||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:hint="@string/room_message_placeholder"
|
||||
android:nextFocusLeft="@id/composerEditText"
|
||||
android:nextFocusUp="@id/composerEditText"
|
||||
android:nextFocusLeft="@id/richTextComposerEditText"
|
||||
android:nextFocusUp="@id/richTextComposerEditText"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:gravity="top"
|
||||
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||
app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
|
||||
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
|
||||
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/plainTextComposerEditText"
|
||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:hint="@string/room_message_placeholder"
|
||||
android:nextFocusLeft="@id/plainTextComposerEditText"
|
||||
android:nextFocusUp="@id/plainTextComposerEditText"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:gravity="top"
|
||||
|
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.attachments
|
||||
|
||||
import com.airbnb.mvrx.test.MavericksTestRule
|
||||
import im.vector.app.test.fakes.FakeVectorFeatures
|
||||
import im.vector.app.test.fakes.FakeVectorPreferences
|
||||
import im.vector.app.test.test
|
||||
import io.mockk.verifyOrder
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
internal class AttachmentTypeSelectorViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mavericksTestRule = MavericksTestRule()
|
||||
|
||||
private val fakeVectorFeatures = FakeVectorFeatures()
|
||||
private val fakeVectorPreferences = FakeVectorPreferences()
|
||||
private val initialState = AttachmentTypeSelectorViewState()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
// Disable all features by default
|
||||
fakeVectorFeatures.givenLocationSharing(isEnabled = false)
|
||||
fakeVectorFeatures.givenVoiceBroadcast(isEnabled = false)
|
||||
fakeVectorPreferences.givenTextFormatting(isEnabled = false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given features are not enabled, then options are not visible`() {
|
||||
createViewModel()
|
||||
.test()
|
||||
.assertStates(
|
||||
listOf(
|
||||
initialState,
|
||||
)
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given location sharing is enabled, then location sharing option is visible`() {
|
||||
fakeVectorFeatures.givenLocationSharing(isEnabled = true)
|
||||
|
||||
createViewModel()
|
||||
.test()
|
||||
.assertStates(
|
||||
listOf(
|
||||
initialState.copy(
|
||||
isLocationVisible = true
|
||||
),
|
||||
)
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given voice broadcast is enabled, then voice broadcast option is visible`() {
|
||||
fakeVectorFeatures.givenVoiceBroadcast(isEnabled = true)
|
||||
|
||||
createViewModel()
|
||||
.test()
|
||||
.assertStates(
|
||||
listOf(
|
||||
initialState.copy(
|
||||
isVoiceBroadcastVisible = true
|
||||
),
|
||||
)
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given text formatting is enabled, then text formatting option is checked`() {
|
||||
fakeVectorPreferences.givenTextFormatting(isEnabled = true)
|
||||
|
||||
createViewModel()
|
||||
.test()
|
||||
.assertStates(
|
||||
listOf(
|
||||
initialState.copy(
|
||||
isTextFormattingEnabled = true
|
||||
),
|
||||
)
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when text formatting is changed, then it updates the UI`() {
|
||||
createViewModel()
|
||||
.apply {
|
||||
handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled = true))
|
||||
}
|
||||
.test()
|
||||
.assertStates(
|
||||
listOf(
|
||||
initialState.copy(
|
||||
isTextFormattingEnabled = true
|
||||
),
|
||||
)
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when text formatting is changed, then it persists the change`() {
|
||||
createViewModel()
|
||||
.apply {
|
||||
handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled = true))
|
||||
handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled = false))
|
||||
}
|
||||
verifyOrder {
|
||||
fakeVectorPreferences.instance.setTextFormattingEnabled(true)
|
||||
fakeVectorPreferences.instance.setTextFormattingEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(): AttachmentTypeSelectorViewModel {
|
||||
return AttachmentTypeSelectorViewModel(
|
||||
initialState,
|
||||
vectorFeatures = fakeVectorFeatures,
|
||||
vectorPreferences = fakeVectorPreferences.instance,
|
||||
)
|
||||
}
|
||||
}
|
@ -42,4 +42,12 @@ class FakeVectorFeatures : VectorFeatures by spyk<DefaultVectorFeatures>() {
|
||||
fun givenCombinedLoginDisabled() {
|
||||
every { isOnboardingCombinedLoginEnabled() } returns false
|
||||
}
|
||||
|
||||
fun givenLocationSharing(isEnabled: Boolean) {
|
||||
every { isLocationSharingEnabled() } returns isEnabled
|
||||
}
|
||||
|
||||
fun givenVoiceBroadcast(isEnabled: Boolean) {
|
||||
every { isVoiceBroadcastEnabled() } returns isEnabled
|
||||
}
|
||||
}
|
||||
|
@ -40,4 +40,7 @@ class FakeVectorPreferences {
|
||||
fun givenIsClientInfoRecordingEnabled(isEnabled: Boolean) {
|
||||
every { instance.isClientInfoRecordingEnabled() } returns isEnabled
|
||||
}
|
||||
|
||||
fun givenTextFormatting(isEnabled: Boolean) =
|
||||
every { instance.isTextFormattingEnabled() } returns isEnabled
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user