Skip to content

[Rich text editor] Add plain text mode and new attachment UI #7459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/7429.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add new UI for selecting an attachment
1 change: 1 addition & 0 deletions changelog.d/7452.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[Rich text editor] Add plain text mode
2 changes: 1 addition & 1 deletion dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<*, *>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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),
}
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -59,7 +53,7 @@ class AttachmentTypeSelectorView(
) : PopupWindow(context) {

interface Callback {
fun onTypeSelected(type: Type)
fun onTypeSelected(type: AttachmentType)
}

private val views: ViewAttachmentTypeSelectorBinding
Expand All @@ -69,14 +63,14 @@ class AttachmentTypeSelectorView(
init {
contentView = inflater.inflate(R.layout.view_attachment_type_selector, null, false)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ Avoid passing null as the view root (needed to resolve layout parameters on the inflated layout's root element)
  • ⚠️ Avoid passing null as the view root (needed to resolve layout parameters on the inflated layout's root element)
  • ⚠️ Avoid passing null as the view root (needed to resolve layout parameters on the inflated layout's root element)
  • ⚠️ Avoid passing null as the view root (needed to resolve layout parameters on the inflated layout's root element)

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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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()
Expand All @@ -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
}
}
}
}
Loading