mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Long press on the whole content item
This commit is contained in:
parent
6cd0fbb614
commit
279820224c
12 changed files with 242 additions and 11 deletions
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.core.utils
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import androidx.core.content.getSystemService
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class CopyToClipboardUseCase @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
|
||||
fun execute(text: CharSequence) {
|
||||
context.getSystemService<ClipboardManager>()
|
||||
?.setPrimaryClip(ClipData.newPlainText("", text))
|
||||
}
|
||||
}
|
|
@ -19,8 +19,6 @@ package im.vector.app.core.utils
|
|||
import android.annotation.TargetApi
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
|
@ -100,8 +98,7 @@ fun requestDisablingBatteryOptimization(activity: Activity, activityResultLaunch
|
|||
* @param toastMessage content of the toast message as a String resource
|
||||
*/
|
||||
fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = true, @StringRes toastMessage: Int = R.string.copied_to_clipboard) {
|
||||
val clipboard = context.getSystemService<ClipboardManager>()!!
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("", text))
|
||||
CopyToClipboardUseCase(context).execute(text)
|
||||
if (showToast) {
|
||||
context.toast(toastMessage)
|
||||
}
|
||||
|
|
|
@ -18,4 +18,6 @@ package im.vector.app.features.settings.devices.v2.details
|
|||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
|
||||
sealed class SessionDetailsAction : VectorViewModelAction
|
||||
sealed class SessionDetailsAction : VectorViewModelAction {
|
||||
data class CopyToClipboard(val content: String) : SessionDetailsAction()
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import com.airbnb.epoxy.EpoxyModelClass
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.extensions.copyOnLongClick
|
||||
|
||||
@EpoxyModelClass
|
||||
abstract class SessionDetailsContentItem : VectorEpoxyModel<SessionDetailsContentItem.Holder>(R.layout.item_session_details_content) {
|
||||
|
@ -38,11 +37,15 @@ abstract class SessionDetailsContentItem : VectorEpoxyModel<SessionDetailsConten
|
|||
@EpoxyAttribute
|
||||
var hasDivider: Boolean = true
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var onLongClickListener: View.OnLongClickListener? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.sessionDetailsContentTitle.text = title
|
||||
holder.sessionDetailsContentDescription.text = description
|
||||
holder.sessionDetailsContentDescription.copyOnLongClick()
|
||||
holder.view.isClickable = onLongClickListener != null
|
||||
holder.view.setOnLongClickListener(onLongClickListener)
|
||||
holder.sessionDetailsContentDivider.isVisible = hasDivider
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.features.settings.devices.v2.details
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.app.R
|
||||
|
@ -34,6 +35,12 @@ class SessionDetailsController @Inject constructor(
|
|||
private val dimensionConverter: DimensionConverter,
|
||||
) : TypedEpoxyController<DeviceInfo>() {
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
interface Callback {
|
||||
fun onItemLongClicked(content: String)
|
||||
}
|
||||
|
||||
override fun buildModels(data: DeviceInfo?) {
|
||||
data?.let { info ->
|
||||
val hasSectionSession = hasSectionSession(data)
|
||||
|
@ -64,6 +71,10 @@ class SessionDetailsController @Inject constructor(
|
|||
title(host.stringProvider.getString(titleResId))
|
||||
description(value)
|
||||
hasDivider(hasDivider)
|
||||
onLongClickListener(View.OnLongClickListener {
|
||||
host.callback?.onItemLongClicked(value)
|
||||
true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import im.vector.app.R
|
|||
import im.vector.app.core.extensions.cleanup
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.platform.showOptimizedSnackbar
|
||||
import im.vector.app.databinding.FragmentSessionDetailsBinding
|
||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||
import javax.inject.Inject
|
||||
|
@ -54,6 +55,7 @@ class SessionDetailsFragment :
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
initToolbar()
|
||||
initSessionDetails()
|
||||
observeViewEvents()
|
||||
}
|
||||
|
||||
private fun initToolbar() {
|
||||
|
@ -63,15 +65,29 @@ class SessionDetailsFragment :
|
|||
}
|
||||
|
||||
private fun initSessionDetails() {
|
||||
sessionDetailsController.callback = object : SessionDetailsController.Callback {
|
||||
override fun onItemLongClicked(content: String) {
|
||||
viewModel.handle(SessionDetailsAction.CopyToClipboard(content))
|
||||
}
|
||||
}
|
||||
views.sessionDetails.configureWith(sessionDetailsController)
|
||||
}
|
||||
|
||||
private fun observeViewEvents() {
|
||||
viewModel.observeViewEvents { viewEvent ->
|
||||
when (viewEvent) {
|
||||
SessionDetailsViewEvent.ContentCopiedToClipboard -> view?.showOptimizedSnackbar(getString(R.string.copied_to_clipboard))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cleanUpSessionDetails()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun cleanUpSessionDetails() {
|
||||
sessionDetailsController.callback = null
|
||||
views.sessionDetails.cleanup()
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.settings.devices.v2.details
|
||||
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
|
||||
sealed class SessionDetailsViewEvent : VectorViewEvents {
|
||||
object ContentCopiedToClipboard : SessionDetailsViewEvent()
|
||||
}
|
|
@ -23,8 +23,8 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.platform.EmptyViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.utils.CopyToClipboardUseCase
|
||||
import im.vector.app.features.settings.devices.v2.overview.GetDeviceFullInfoUseCase
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -32,7 +32,8 @@ import kotlinx.coroutines.flow.onEach
|
|||
class SessionDetailsViewModel @AssistedInject constructor(
|
||||
@Assisted val initialState: SessionDetailsViewState,
|
||||
private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
|
||||
) : VectorViewModel<SessionDetailsViewState, SessionDetailsAction, EmptyViewEvents>(initialState) {
|
||||
private val copyToClipboardUseCase: CopyToClipboardUseCase,
|
||||
) : VectorViewModel<SessionDetailsViewState, SessionDetailsAction, SessionDetailsViewEvent>(initialState) {
|
||||
|
||||
companion object : MavericksViewModelFactory<SessionDetailsViewModel, SessionDetailsViewState> by hiltMavericksViewModelFactory()
|
||||
|
||||
|
@ -52,6 +53,13 @@ class SessionDetailsViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun handle(action: SessionDetailsAction) {
|
||||
TODO("Implement when adding the first action")
|
||||
return when (action) {
|
||||
is SessionDetailsAction.CopyToClipboard -> handleCopyToClipboard(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyToClipboard(copyToClipboard: SessionDetailsAction.CopyToClipboard) {
|
||||
copyToClipboardUseCase.execute(copyToClipboard.content)
|
||||
_viewEvents.post(SessionDetailsViewEvent.ContentCopiedToClipboard)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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.core.utils
|
||||
|
||||
import android.content.ClipData
|
||||
import im.vector.app.test.fakes.FakeContext
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkAll
|
||||
import io.mockk.verify
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
private const val A_TEXT = "text"
|
||||
|
||||
class CopyToClipboardUseCaseTest {
|
||||
|
||||
private val fakeContext = FakeContext()
|
||||
|
||||
private val copyToClipboardUseCase = CopyToClipboardUseCase(
|
||||
context = fakeContext.instance
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic(ClipData::class)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a text when executing the use case then the text is copied into the clipboard`() {
|
||||
// Given
|
||||
val clipboardManager = fakeContext.givenClipboardManager()
|
||||
clipboardManager.givenSetPrimaryClip()
|
||||
val clipData = mockk<ClipData>()
|
||||
every { ClipData.newPlainText(any(), any()) } returns clipData
|
||||
|
||||
// When
|
||||
copyToClipboardUseCase.execute(A_TEXT)
|
||||
|
||||
// Then
|
||||
clipboardManager.verifySetPrimaryClip(clipData)
|
||||
verify { ClipData.newPlainText("", A_TEXT) }
|
||||
}
|
||||
}
|
|
@ -18,12 +18,15 @@ package im.vector.app.features.settings.devices.v2.details
|
|||
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.test.MvRxTestRule
|
||||
import im.vector.app.core.utils.CopyToClipboardUseCase
|
||||
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
|
||||
import im.vector.app.features.settings.devices.v2.overview.GetDeviceFullInfoUseCase
|
||||
import im.vector.app.test.test
|
||||
import im.vector.app.test.testDispatcher
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.junit.Rule
|
||||
|
@ -31,6 +34,7 @@ import org.junit.Test
|
|||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||
|
||||
private const val A_SESSION_ID = "session-id"
|
||||
private const val A_TEXT = "text"
|
||||
|
||||
class SessionDetailsViewModelTest {
|
||||
|
||||
|
@ -41,10 +45,12 @@ class SessionDetailsViewModelTest {
|
|||
deviceId = A_SESSION_ID
|
||||
)
|
||||
private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>()
|
||||
private val copyToClipboardUseCase = mockk<CopyToClipboardUseCase>()
|
||||
|
||||
private fun createViewModel() = SessionDetailsViewModel(
|
||||
initialState = SessionDetailsViewState(args),
|
||||
getDeviceFullInfoUseCase = getDeviceFullInfoUseCase
|
||||
getDeviceFullInfoUseCase = getDeviceFullInfoUseCase,
|
||||
copyToClipboardUseCase = copyToClipboardUseCase,
|
||||
)
|
||||
|
||||
@Test
|
||||
|
@ -68,4 +74,26 @@ class SessionDetailsViewModelTest {
|
|||
.finish()
|
||||
verify { getDeviceFullInfoUseCase.execute(A_SESSION_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given copyToClipboard action when viewModel handle it then related use case is executed and viewEvent is updated`() {
|
||||
// Given
|
||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||
val deviceInfo = mockk<DeviceInfo>()
|
||||
every { deviceFullInfo.deviceInfo } returns deviceInfo
|
||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(deviceFullInfo)
|
||||
val action = SessionDetailsAction.CopyToClipboard(A_TEXT)
|
||||
every { copyToClipboardUseCase.execute(any()) } just runs
|
||||
|
||||
// When
|
||||
val viewModel = createViewModel()
|
||||
val viewModelTest = viewModel.test()
|
||||
viewModel.handle(action)
|
||||
|
||||
// Then
|
||||
viewModelTest
|
||||
.assertEvent { it is SessionDetailsViewEvent.ContentCopiedToClipboard }
|
||||
.finish()
|
||||
verify { copyToClipboardUseCase.execute(A_TEXT) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.test.fakes
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
|
||||
class FakeClipboardManager {
|
||||
val instance = mockk<ClipboardManager>()
|
||||
|
||||
fun givenSetPrimaryClip() {
|
||||
every { instance.setPrimaryClip(any()) } just runs
|
||||
}
|
||||
|
||||
fun verifySetPrimaryClip(clipData: ClipData) {
|
||||
verify { instance.setPrimaryClip(clipData) }
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.test.fakes
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
@ -74,4 +75,10 @@ class FakeContext(
|
|||
fun givenStartActivity(intent: Intent) {
|
||||
every { instance.startActivity(intent) } just runs
|
||||
}
|
||||
|
||||
fun givenClipboardManager(): FakeClipboardManager {
|
||||
val fakeClipboardManager = FakeClipboardManager()
|
||||
givenService(Context.CLIPBOARD_SERVICE, ClipboardManager::class.java, fakeClipboardManager.instance)
|
||||
return fakeClipboardManager
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue