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_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>
<plurals name="message_reaction_show_more">
<item quantity="one">"%1$d more"</item>

View File

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

View File

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

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

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
// 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

View File

@ -40,6 +40,7 @@ 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.parentFragmentViewModel
@ -63,6 +64,10 @@ 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.AttachmentsHelper
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.voice.VoiceFailure
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@ -161,6 +167,7 @@ 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: AttachmentTypeSelectorSharedActionViewModel by viewModels()
private val composer: MessageComposerView get() {
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) {
handleShareData()
}
@ -299,21 +311,25 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
}
composer.callback = object : PlainTextComposerLayout.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() {
@ -662,20 +678,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(),
@ -685,11 +701,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 {

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() {
every { isOnboardingCombinedLoginEnabled() } returns false
}
fun givenLocationSharing(isEnabled: Boolean) {
every { isLocationSharingEnabled() } returns isEnabled
}
fun givenVoiceBroadcast(isEnabled: Boolean) {
every { isVoiceBroadcastEnabled() } returns isEnabled
}
}