Merge pull request #7429 from jonnyandrew/feat/PSU-919-attachments-v2

Add new UI for selecting an attachment type
This commit is contained in:
jonnyandrew 2022-10-25 11:46:34 +01:00 committed by GitHub
commit 7cc06fa5ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 486 additions and 61 deletions

1
changelog.d/7429.feature Normal file
View File

@ -0,0 +1 @@
Add new UI for selecting an attachment

View File

@ -3205,6 +3205,15 @@
<string name="tooltip_attachment_location">Share location</string> <string name="tooltip_attachment_location">Share location</string>
<string name="tooltip_attachment_voice_broadcast">Start a voice broadcast</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="message_reaction_show_less">Show less</string> <string name="message_reaction_show_less">Show less</string>
<plurals name="message_reaction_show_more"> <plurals name="message_reaction_show_more">
<item quantity="one">"%1$d more"</item> <item quantity="one">"%1$d more"</item>

View File

@ -22,6 +22,7 @@ import dagger.hilt.InstallIn
import dagger.multibindings.IntoMap import dagger.multibindings.IntoMap
import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel
import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel 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.auth.ReAuthViewModel
import im.vector.app.features.call.VectorCallViewModel import im.vector.app.features.call.VectorCallViewModel
import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.conference.JitsiCallViewModel
@ -677,4 +678,9 @@ interface MavericksViewModelModule {
@IntoMap @IntoMap
@MavericksViewModelKey(VectorSettingsLabsViewModel::class) @MavericksViewModelKey(VectorSettingsLabsViewModel::class)
fun vectorSettingsLabsViewModelFactory(factory: VectorSettingsLabsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun vectorSettingsLabsViewModelFactory(factory: VectorSettingsLabsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(AttachmentTypeSelectorViewModel::class)
fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
} }

View File

@ -38,6 +38,10 @@ class BottomSheetActionButton @JvmOverloads constructor(
) : FrameLayout(context, attrs, defStyleAttr) { ) : FrameLayout(context, attrs, defStyleAttr) {
val views: ViewBottomSheetActionButtonBinding val views: ViewBottomSheetActionButtonBinding
override fun setOnClickListener(l: OnClickListener?) {
views.bottomSheetActionClickableZone.setOnClickListener(l)
}
var title: String? = null var title: String? = null
set(value) { set(value) {
field = value field = value

View File

@ -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),
}

View File

@ -0,0 +1,80 @@
/*
* 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.fragmentViewModel
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
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 fragmentViewModel()
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()
}
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) }
}
private fun onAttachmentSelected(attachmentType: AttachmentType) {
val action = AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction(attachmentType)
sharedActionViewModel.post(action)
dismiss()
}
companion object {
fun show(fragmentManager: FragmentManager) {
val bottomSheet = AttachmentTypeSelectorBottomSheet()
bottomSheet.show(fragmentManager, "AttachmentTypeSelectorBottomSheet")
}
}
}

View File

@ -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
}

View File

@ -30,17 +30,11 @@ import android.view.animation.TranslateAnimation
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.PopupWindow import android.widget.PopupWindow
import androidx.annotation.StringRes
import androidx.appcompat.widget.TooltipCompat import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.doOnNextLayout import androidx.core.view.doOnNextLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.onClick 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.databinding.ViewAttachmentTypeSelectorBinding
import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback
import kotlin.math.max import kotlin.math.max
@ -59,7 +53,7 @@ class AttachmentTypeSelectorView(
) : PopupWindow(context) { ) : PopupWindow(context) {
interface Callback { interface Callback {
fun onTypeSelected(type: Type) fun onTypeSelected(type: AttachmentType)
} }
private val views: ViewAttachmentTypeSelectorBinding private val views: ViewAttachmentTypeSelectorBinding
@ -69,14 +63,14 @@ class AttachmentTypeSelectorView(
init { init {
contentView = inflater.inflate(R.layout.view_attachment_type_selector, null, false) contentView = inflater.inflate(R.layout.view_attachment_type_selector, null, false)
views = ViewAttachmentTypeSelectorBinding.bind(contentView) views = ViewAttachmentTypeSelectorBinding.bind(contentView)
views.attachmentGalleryButton.configure(Type.GALLERY) views.attachmentGalleryButton.configure(AttachmentType.GALLERY)
views.attachmentCameraButton.configure(Type.CAMERA) views.attachmentCameraButton.configure(AttachmentType.CAMERA)
views.attachmentFileButton.configure(Type.FILE) views.attachmentFileButton.configure(AttachmentType.FILE)
views.attachmentStickersButton.configure(Type.STICKER) views.attachmentStickersButton.configure(AttachmentType.STICKER)
views.attachmentContactButton.configure(Type.CONTACT) views.attachmentContactButton.configure(AttachmentType.CONTACT)
views.attachmentPollButton.configure(Type.POLL) views.attachmentPollButton.configure(AttachmentType.POLL)
views.attachmentLocationButton.configure(Type.LOCATION) views.attachmentLocationButton.configure(AttachmentType.LOCATION)
views.attachmentVoiceBroadcast.configure(Type.VOICE_BROADCAST) views.attachmentVoiceBroadcast.configure(AttachmentType.VOICE_BROADCAST)
width = LinearLayout.LayoutParams.MATCH_PARENT width = LinearLayout.LayoutParams.MATCH_PARENT
height = LinearLayout.LayoutParams.WRAP_CONTENT height = LinearLayout.LayoutParams.WRAP_CONTENT
animationStyle = 0 animationStyle = 0
@ -127,16 +121,16 @@ class AttachmentTypeSelectorView(
} }
} }
fun setAttachmentVisibility(type: Type, isVisible: Boolean) { fun setAttachmentVisibility(type: AttachmentType, isVisible: Boolean) {
when (type) { when (type) {
Type.CAMERA -> views.attachmentCameraButton AttachmentType.CAMERA -> views.attachmentCameraButton
Type.GALLERY -> views.attachmentGalleryButton AttachmentType.GALLERY -> views.attachmentGalleryButton
Type.FILE -> views.attachmentFileButton AttachmentType.FILE -> views.attachmentFileButton
Type.STICKER -> views.attachmentStickersButton AttachmentType.STICKER -> views.attachmentStickersButton
Type.CONTACT -> views.attachmentContactButton AttachmentType.CONTACT -> views.attachmentContactButton
Type.POLL -> views.attachmentPollButton AttachmentType.POLL -> views.attachmentPollButton
Type.LOCATION -> views.attachmentLocationButton AttachmentType.LOCATION -> views.attachmentLocationButton
Type.VOICE_BROADCAST -> views.attachmentVoiceBroadcast AttachmentType.VOICE_BROADCAST -> views.attachmentVoiceBroadcast
}.let { }.let {
it.isVisible = isVisible it.isVisible = isVisible
} }
@ -200,13 +194,13 @@ class AttachmentTypeSelectorView(
return Pair(x, y) return Pair(x, y)
} }
private fun ImageButton.configure(type: Type): ImageButton { private fun ImageButton.configure(type: AttachmentType): ImageButton {
this.setOnClickListener(TypeClickListener(type)) this.setOnClickListener(TypeClickListener(type))
TooltipCompat.setTooltipText(this, context.getString(type.tooltipRes)) TooltipCompat.setTooltipText(this, context.getString(attachmentTooltipLabels.getValue(type)))
return this 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) { override fun onClick(v: View) {
dismiss() dismiss()
@ -217,14 +211,18 @@ class AttachmentTypeSelectorView(
/** /**
* The all possible types to pick with their required permissions and tooltip resource. * The all possible types to pick with their required permissions and tooltip resource.
*/ */
enum class Type(val permissions: List<String>, @StringRes val tooltipRes: Int) { private companion object {
CAMERA(PERMISSIONS_FOR_TAKING_PHOTO, R.string.tooltip_attachment_photo), private val attachmentTooltipLabels: Map<AttachmentType, Int> = AttachmentType.values().associateWith {
GALLERY(PERMISSIONS_EMPTY, R.string.tooltip_attachment_gallery), when (it) {
FILE(PERMISSIONS_EMPTY, R.string.tooltip_attachment_file), AttachmentType.CAMERA -> R.string.tooltip_attachment_photo
STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker), AttachmentType.GALLERY -> R.string.tooltip_attachment_gallery
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact), AttachmentType.FILE -> R.string.tooltip_attachment_file
POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll), AttachmentType.STICKER -> R.string.tooltip_attachment_sticker
LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, R.string.tooltip_attachment_location), AttachmentType.CONTACT -> R.string.tooltip_attachment_contact
VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST, R.string.tooltip_attachment_voice_broadcast), AttachmentType.POLL -> R.string.tooltip_attachment_poll
AttachmentType.LOCATION -> R.string.tooltip_attachment_location
AttachmentType.VOICE_BROADCAST -> R.string.tooltip_attachment_voice_broadcast
}
}
} }
} }

View File

@ -0,0 +1,59 @@
/*
* 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.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.VectorFeatures
class AttachmentTypeSelectorViewModel @AssistedInject constructor(
@Assisted initialState: AttachmentTypeSelectorViewState,
private val vectorFeatures: VectorFeatures,
) : VectorViewModel<AttachmentTypeSelectorViewState, EmptyAction, 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: EmptyAction) {
// do nothing
}
init {
setState {
copy(
isLocationVisible = vectorFeatures.isLocationSharingEnabled(),
isVoiceBroadcastVisible = vectorFeatures.isVoiceBroadcastEnabled(),
)
}
}
}
data class AttachmentTypeSelectorViewState(
val isLocationVisible: Boolean = false,
val isVoiceBroadcastVisible: Boolean = false,
) : MavericksState

View File

@ -54,7 +54,7 @@ class AttachmentsHelper(
private var captureUri: Uri? = null 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. // 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 // Restorable

View File

@ -40,6 +40,7 @@ import androidx.core.text.buildSpannedString
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.parentFragmentViewModel
@ -63,6 +64,10 @@ import im.vector.app.core.utils.onPermissionDeniedDialog
import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.FragmentComposerBinding import im.vector.app.databinding.FragmentComposerBinding
import im.vector.app.features.VectorFeatures 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.AttachmentTypeSelectorView
import im.vector.app.features.attachments.AttachmentsHelper import im.vector.app.features.attachments.AttachmentsHelper
import im.vector.app.features.attachments.ContactAttachment import im.vector.app.features.attachments.ContactAttachment
@ -92,6 +97,7 @@ import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.share.SharedData import im.vector.app.features.share.SharedData
import im.vector.app.features.voice.VoiceFailure import im.vector.app.features.voice.VoiceFailure
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -161,6 +167,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
private val timelineViewModel: TimelineViewModel by parentFragmentViewModel() private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel() private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel()
private lateinit var sharedActionViewModel: MessageSharedActionViewModel private lateinit var sharedActionViewModel: MessageSharedActionViewModel
private val attachmentViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
private val composer: MessageComposerView get() { private val composer: MessageComposerView get() {
return if (vectorPreferences.isRichTextEditorEnabled()) { return if (vectorPreferences.isRichTextEditorEnabled()) {
@ -219,6 +226,11 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
} }
} }
attachmentViewModel.stream()
.filterIsInstance<AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction>()
.onEach { onTypeSelected(it.attachmentType) }
.launchIn(lifecycleScope)
if (savedInstanceState != null) { if (savedInstanceState != null) {
handleShareData() handleShareData()
} }
@ -299,22 +311,26 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
} }
composer.callback = object : PlainTextComposerLayout.Callback { composer.callback = object : PlainTextComposerLayout.Callback {
override fun onAddAttachment() { override fun onAddAttachment() {
if (vectorPreferences.isRichTextEditorEnabled()) {
AttachmentTypeSelectorBottomSheet.show(childFragmentManager)
} else {
if (!::attachmentTypeSelector.isInitialized) { if (!::attachmentTypeSelector.isInitialized) {
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment) attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment)
attachmentTypeSelector.setAttachmentVisibility( attachmentTypeSelector.setAttachmentVisibility(
AttachmentTypeSelectorView.Type.LOCATION, AttachmentType.LOCATION,
vectorFeatures.isLocationSharingEnabled(), vectorFeatures.isLocationSharingEnabled(),
) )
attachmentTypeSelector.setAttachmentVisibility( attachmentTypeSelector.setAttachmentVisibility(
AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine() AttachmentType.POLL, !isThreadTimeLine()
) )
attachmentTypeSelector.setAttachmentVisibility( attachmentTypeSelector.setAttachmentVisibility(
AttachmentTypeSelectorView.Type.VOICE_BROADCAST, AttachmentType.VOICE_BROADCAST,
vectorPreferences.isVoiceBroadcastEnabled(), // TODO check user permission vectorPreferences.isVoiceBroadcastEnabled(), // TODO check user permission
) )
} }
attachmentTypeSelector.show(composer.attachmentButton) attachmentTypeSelector.show(composer.attachmentButton)
} }
}
override fun onExpandOrCompactChange() { override fun onExpandOrCompactChange() {
composer.emojiButton?.isVisible = isEmojiKeyboardVisible composer.emojiButton?.isVisible = isEmojiKeyboardVisible
@ -662,20 +678,20 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
} }
} }
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { private fun launchAttachmentProcess(type: AttachmentType) {
when (type) { when (type) {
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera( AttachmentType.CAMERA -> attachmentsHelper.openCamera(
activity = requireActivity(), activity = requireActivity(),
vectorPreferences = vectorPreferences, vectorPreferences = vectorPreferences,
cameraActivityResultLauncher = attachmentCameraActivityResultLauncher, cameraActivityResultLauncher = attachmentCameraActivityResultLauncher,
cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher
) )
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) AttachmentType.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) AttachmentType.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher) AttachmentType.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
AttachmentTypeSelectorView.Type.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment) AttachmentType.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment)
AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomId, null, PollMode.CREATE) AttachmentType.POLL -> navigator.openCreatePoll(requireContext(), roomId, null, PollMode.CREATE)
AttachmentTypeSelectorView.Type.LOCATION -> { AttachmentType.LOCATION -> {
navigator navigator
.openLocationSharing( .openLocationSharing(
context = requireContext(), context = requireContext(),
@ -685,11 +701,11 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
locationOwnerId = session.myUserId 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)) { if (checkPermissions(type.permissions, requireActivity(), typeSelectedActivityResultLauncher)) {
launchAttachmentProcess(type) launchAttachmentProcess(type)
} else { } else {

View File

@ -0,0 +1,86 @@
<?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"
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" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,91 @@
/*
* 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.test
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 initialState = AttachmentTypeSelectorViewState()
@Before
fun setUp() {
// Disable all features by default
fakeVectorFeatures.givenLocationSharing(isEnabled = false)
fakeVectorFeatures.givenVoiceBroadcast(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()
}
private fun createViewModel(): AttachmentTypeSelectorViewModel {
return AttachmentTypeSelectorViewModel(
initialState,
vectorFeatures = fakeVectorFeatures,
)
}
}

View File

@ -42,4 +42,12 @@ class FakeVectorFeatures : VectorFeatures by spyk<DefaultVectorFeatures>() {
fun givenCombinedLoginDisabled() { fun givenCombinedLoginDisabled() {
every { isOnboardingCombinedLoginEnabled() } returns false every { isOnboardingCombinedLoginEnabled() } returns false
} }
fun givenLocationSharing(isEnabled: Boolean) {
every { isLocationSharingEnabled() } returns isEnabled
}
fun givenVoiceBroadcast(isEnabled: Boolean) {
every { isVoiceBroadcastEnabled() } returns isEnabled
}
} }