Skip to content

Commit c776aae

Browse files
authored
[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](material-components/material-components-android#437) - [Github issue](material-components/material-components-android#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 17c43c9. * 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
1 parent bdfc96f commit c776aae

25 files changed

+797
-102
lines changed

changelog.d/7429.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add new UI for selecting an attachment

changelog.d/7452.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[Rich text editor] Add plain text mode

dependencies.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ ext.libs = [
101101
],
102102
element : [
103103
'opusencoder' : "io.element.android:opusencoder:1.1.0",
104-
'wysiwyg' : "io.element.android:wysiwyg:0.2.1"
104+
'wysiwyg' : "io.element.android:wysiwyg:0.4.0"
105105
],
106106
squareup : [
107107
'moshi' : "com.squareup.moshi:moshi:$moshi",

library/ui-strings/src/main/res/values/strings.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3205,6 +3205,16 @@
32053205
<string name="tooltip_attachment_location">Share location</string>
32063206
<string name="tooltip_attachment_voice_broadcast">Start a voice broadcast</string>
32073207

3208+
<string name="attachment_type_selector_gallery">Photo library</string>
3209+
<string name="attachment_type_selector_sticker">Stickers</string>
3210+
<string name="attachment_type_selector_file">Attachments</string>
3211+
<string name="attachment_type_selector_voice_broadcast">Voice broadcast</string>
3212+
<string name="attachment_type_selector_poll">Polls</string>
3213+
<string name="attachment_type_selector_location">Location</string>
3214+
<string name="attachment_type_selector_camera">Camera</string>
3215+
<string name="attachment_type_selector_contact">Contact</string>
3216+
<string name="attachment_type_selector_text_formatting">Text formatting</string>
3217+
32083218
<string name="message_reaction_show_less">Show less</string>
32093219
<plurals name="message_reaction_show_more">
32103220
<item quantity="one">"%1$d more"</item>

vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import dagger.hilt.InstallIn
2222
import dagger.multibindings.IntoMap
2323
import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel
2424
import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel
25+
import im.vector.app.features.attachments.AttachmentTypeSelectorViewModel
2526
import im.vector.app.features.auth.ReAuthViewModel
2627
import im.vector.app.features.call.VectorCallViewModel
2728
import im.vector.app.features.call.conference.JitsiCallViewModel
@@ -677,4 +678,9 @@ interface MavericksViewModelModule {
677678
@IntoMap
678679
@MavericksViewModelKey(VectorSettingsLabsViewModel::class)
679680
fun vectorSettingsLabsViewModelFactory(factory: VectorSettingsLabsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
681+
682+
@Binds
683+
@IntoMap
684+
@MavericksViewModelKey(AttachmentTypeSelectorViewModel::class)
685+
fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
680686
}

vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ class BottomSheetActionButton @JvmOverloads constructor(
3838
) : FrameLayout(context, attrs, defStyleAttr) {
3939
val views: ViewBottomSheetActionButtonBinding
4040

41+
override fun setOnClickListener(l: OnClickListener?) {
42+
views.bottomSheetActionClickableZone.setOnClickListener(l)
43+
}
44+
4145
var title: String? = null
4246
set(value) {
4347
field = value
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.attachments
18+
19+
import im.vector.app.core.utils.PERMISSIONS_EMPTY
20+
import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
21+
import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
22+
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
23+
import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_BROADCAST
24+
25+
/**
26+
* The all possible types to pick with their required permissions.
27+
*/
28+
enum class AttachmentType(val permissions: List<String>) {
29+
CAMERA(PERMISSIONS_FOR_TAKING_PHOTO),
30+
GALLERY(PERMISSIONS_EMPTY),
31+
FILE(PERMISSIONS_EMPTY),
32+
STICKER(PERMISSIONS_EMPTY),
33+
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT),
34+
POLL(PERMISSIONS_EMPTY),
35+
LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING),
36+
VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST),
37+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.attachments
18+
19+
import android.os.Bundle
20+
import android.view.LayoutInflater
21+
import android.view.View
22+
import android.view.ViewGroup
23+
import androidx.core.view.isVisible
24+
import androidx.fragment.app.FragmentManager
25+
import androidx.fragment.app.viewModels
26+
import com.airbnb.mvrx.parentFragmentViewModel
27+
import com.airbnb.mvrx.withState
28+
import dagger.hilt.android.AndroidEntryPoint
29+
import im.vector.app.R
30+
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
31+
import im.vector.app.databinding.BottomSheetAttachmentTypeSelectorBinding
32+
import im.vector.app.features.home.room.detail.TimelineViewModel
33+
34+
@AndroidEntryPoint
35+
class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetAttachmentTypeSelectorBinding>() {
36+
37+
private val viewModel: AttachmentTypeSelectorViewModel by parentFragmentViewModel()
38+
private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
39+
private val sharedActionViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels(
40+
ownerProducer = { requireParentFragment() }
41+
)
42+
43+
override val showExpanded = true
44+
45+
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetAttachmentTypeSelectorBinding {
46+
return BottomSheetAttachmentTypeSelectorBinding.inflate(inflater, container, false)
47+
}
48+
49+
override fun invalidate() = withState(viewModel, timelineViewModel) { viewState, timelineState ->
50+
super.invalidate()
51+
views.location.isVisible = viewState.isLocationVisible
52+
views.voiceBroadcast.isVisible = viewState.isVoiceBroadcastVisible
53+
views.poll.isVisible = !timelineState.isThreadTimeline()
54+
views.textFormatting.isChecked = viewState.isTextFormattingEnabled
55+
views.textFormatting.setCompoundDrawablesRelativeWithIntrinsicBounds(
56+
if (viewState.isTextFormattingEnabled) {
57+
R.drawable.ic_text_formatting
58+
} else {
59+
R.drawable.ic_text_formatting_disabled
60+
}, 0, 0, 0
61+
)
62+
}
63+
64+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
65+
super.onViewCreated(view, savedInstanceState)
66+
views.gallery.debouncedClicks { onAttachmentSelected(AttachmentType.GALLERY) }
67+
views.stickers.debouncedClicks { onAttachmentSelected(AttachmentType.STICKER) }
68+
views.file.debouncedClicks { onAttachmentSelected(AttachmentType.FILE) }
69+
views.voiceBroadcast.debouncedClicks { onAttachmentSelected(AttachmentType.VOICE_BROADCAST) }
70+
views.poll.debouncedClicks { onAttachmentSelected(AttachmentType.POLL) }
71+
views.location.debouncedClicks { onAttachmentSelected(AttachmentType.LOCATION) }
72+
views.camera.debouncedClicks { onAttachmentSelected(AttachmentType.CAMERA) }
73+
views.contact.debouncedClicks { onAttachmentSelected(AttachmentType.CONTACT) }
74+
views.textFormatting.setOnCheckedChangeListener { _, isChecked -> onTextFormattingToggled(isChecked) }
75+
}
76+
77+
private fun onAttachmentSelected(attachmentType: AttachmentType) {
78+
val action = AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction(attachmentType)
79+
sharedActionViewModel.post(action)
80+
dismiss()
81+
}
82+
83+
private fun onTextFormattingToggled(isEnabled: Boolean) =
84+
viewModel.handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled))
85+
86+
companion object {
87+
fun show(fragmentManager: FragmentManager) {
88+
val bottomSheet = AttachmentTypeSelectorBottomSheet()
89+
bottomSheet.show(fragmentManager, "AttachmentTypeSelectorBottomSheet")
90+
}
91+
}
92+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package im.vector.app.features.attachments
18+
19+
import im.vector.app.core.platform.VectorSharedAction
20+
import im.vector.app.core.platform.VectorSharedActionViewModel
21+
import javax.inject.Inject
22+
23+
class AttachmentTypeSelectorSharedActionViewModel @Inject constructor() :
24+
VectorSharedActionViewModel<AttachmentTypeSelectorSharedAction>()
25+
26+
sealed interface AttachmentTypeSelectorSharedAction : VectorSharedAction {
27+
data class SelectAttachmentTypeAction(
28+
val attachmentType: AttachmentType
29+
) : AttachmentTypeSelectorSharedAction
30+
}

vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,11 @@ import android.view.animation.TranslateAnimation
3030
import android.widget.ImageButton
3131
import android.widget.LinearLayout
3232
import android.widget.PopupWindow
33-
import androidx.annotation.StringRes
3433
import androidx.appcompat.widget.TooltipCompat
3534
import androidx.core.view.doOnNextLayout
3635
import androidx.core.view.isVisible
3736
import im.vector.app.R
3837
import im.vector.app.core.epoxy.onClick
39-
import im.vector.app.core.utils.PERMISSIONS_EMPTY
40-
import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
41-
import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
42-
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
43-
import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_BROADCAST
4438
import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding
4539
import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback
4640
import kotlin.math.max
@@ -59,7 +53,7 @@ class AttachmentTypeSelectorView(
5953
) : PopupWindow(context) {
6054

6155
interface Callback {
62-
fun onTypeSelected(type: Type)
56+
fun onTypeSelected(type: AttachmentType)
6357
}
6458

6559
private val views: ViewAttachmentTypeSelectorBinding
@@ -69,14 +63,14 @@ class AttachmentTypeSelectorView(
6963
init {
7064
contentView = inflater.inflate(R.layout.view_attachment_type_selector, null, false)
7165
views = ViewAttachmentTypeSelectorBinding.bind(contentView)
72-
views.attachmentGalleryButton.configure(Type.GALLERY)
73-
views.attachmentCameraButton.configure(Type.CAMERA)
74-
views.attachmentFileButton.configure(Type.FILE)
75-
views.attachmentStickersButton.configure(Type.STICKER)
76-
views.attachmentContactButton.configure(Type.CONTACT)
77-
views.attachmentPollButton.configure(Type.POLL)
78-
views.attachmentLocationButton.configure(Type.LOCATION)
79-
views.attachmentVoiceBroadcast.configure(Type.VOICE_BROADCAST)
66+
views.attachmentGalleryButton.configure(AttachmentType.GALLERY)
67+
views.attachmentCameraButton.configure(AttachmentType.CAMERA)
68+
views.attachmentFileButton.configure(AttachmentType.FILE)
69+
views.attachmentStickersButton.configure(AttachmentType.STICKER)
70+
views.attachmentContactButton.configure(AttachmentType.CONTACT)
71+
views.attachmentPollButton.configure(AttachmentType.POLL)
72+
views.attachmentLocationButton.configure(AttachmentType.LOCATION)
73+
views.attachmentVoiceBroadcast.configure(AttachmentType.VOICE_BROADCAST)
8074
width = LinearLayout.LayoutParams.MATCH_PARENT
8175
height = LinearLayout.LayoutParams.WRAP_CONTENT
8276
animationStyle = 0
@@ -127,16 +121,16 @@ class AttachmentTypeSelectorView(
127121
}
128122
}
129123

130-
fun setAttachmentVisibility(type: Type, isVisible: Boolean) {
124+
fun setAttachmentVisibility(type: AttachmentType, isVisible: Boolean) {
131125
when (type) {
132-
Type.CAMERA -> views.attachmentCameraButton
133-
Type.GALLERY -> views.attachmentGalleryButton
134-
Type.FILE -> views.attachmentFileButton
135-
Type.STICKER -> views.attachmentStickersButton
136-
Type.CONTACT -> views.attachmentContactButton
137-
Type.POLL -> views.attachmentPollButton
138-
Type.LOCATION -> views.attachmentLocationButton
139-
Type.VOICE_BROADCAST -> views.attachmentVoiceBroadcast
126+
AttachmentType.CAMERA -> views.attachmentCameraButton
127+
AttachmentType.GALLERY -> views.attachmentGalleryButton
128+
AttachmentType.FILE -> views.attachmentFileButton
129+
AttachmentType.STICKER -> views.attachmentStickersButton
130+
AttachmentType.CONTACT -> views.attachmentContactButton
131+
AttachmentType.POLL -> views.attachmentPollButton
132+
AttachmentType.LOCATION -> views.attachmentLocationButton
133+
AttachmentType.VOICE_BROADCAST -> views.attachmentVoiceBroadcast
140134
}.let {
141135
it.isVisible = isVisible
142136
}
@@ -200,13 +194,13 @@ class AttachmentTypeSelectorView(
200194
return Pair(x, y)
201195
}
202196

203-
private fun ImageButton.configure(type: Type): ImageButton {
197+
private fun ImageButton.configure(type: AttachmentType): ImageButton {
204198
this.setOnClickListener(TypeClickListener(type))
205-
TooltipCompat.setTooltipText(this, context.getString(type.tooltipRes))
199+
TooltipCompat.setTooltipText(this, context.getString(attachmentTooltipLabels.getValue(type)))
206200
return this
207201
}
208202

209-
private inner class TypeClickListener(private val type: Type) : View.OnClickListener {
203+
private inner class TypeClickListener(private val type: AttachmentType) : View.OnClickListener {
210204

211205
override fun onClick(v: View) {
212206
dismiss()
@@ -217,14 +211,18 @@ class AttachmentTypeSelectorView(
217211
/**
218212
* The all possible types to pick with their required permissions and tooltip resource.
219213
*/
220-
enum class Type(val permissions: List<String>, @StringRes val tooltipRes: Int) {
221-
CAMERA(PERMISSIONS_FOR_TAKING_PHOTO, R.string.tooltip_attachment_photo),
222-
GALLERY(PERMISSIONS_EMPTY, R.string.tooltip_attachment_gallery),
223-
FILE(PERMISSIONS_EMPTY, R.string.tooltip_attachment_file),
224-
STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker),
225-
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact),
226-
POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll),
227-
LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, R.string.tooltip_attachment_location),
228-
VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST, R.string.tooltip_attachment_voice_broadcast),
214+
private companion object {
215+
private val attachmentTooltipLabels: Map<AttachmentType, Int> = AttachmentType.values().associateWith {
216+
when (it) {
217+
AttachmentType.CAMERA -> R.string.tooltip_attachment_photo
218+
AttachmentType.GALLERY -> R.string.tooltip_attachment_gallery
219+
AttachmentType.FILE -> R.string.tooltip_attachment_file
220+
AttachmentType.STICKER -> R.string.tooltip_attachment_sticker
221+
AttachmentType.CONTACT -> R.string.tooltip_attachment_contact
222+
AttachmentType.POLL -> R.string.tooltip_attachment_poll
223+
AttachmentType.LOCATION -> R.string.tooltip_attachment_location
224+
AttachmentType.VOICE_BROADCAST -> R.string.tooltip_attachment_voice_broadcast
225+
}
226+
}
229227
}
230228
}

0 commit comments

Comments
 (0)