diff --git a/core/data/src/main/java/org/mozilla/social/core/data/RepositoryModule.kt b/core/data/src/main/java/org/mozilla/social/core/data/RepositoryModule.kt index 8fecf69f8..cb47257e8 100644 --- a/core/data/src/main/java/org/mozilla/social/core/data/RepositoryModule.kt +++ b/core/data/src/main/java/org/mozilla/social/core/data/RepositoryModule.kt @@ -7,7 +7,6 @@ import org.mozilla.social.core.data.repository.InstanceRepository import org.mozilla.social.core.data.repository.MediaRepository import org.mozilla.social.core.data.repository.OauthRepository import org.mozilla.social.core.data.repository.RecommendationRepository -import org.mozilla.social.core.data.repository.ReportRepository import org.mozilla.social.core.data.repository.SearchRepository import org.mozilla.social.core.data.repository.StatusRepository import org.mozilla.social.core.data.repository.TimelineRepository @@ -25,6 +24,5 @@ fun repositoryModule(isDebug: Boolean) = module { single { RecommendationRepository(get()) } single { AppRepository(get()) } single { InstanceRepository(get()) } - single { ReportRepository(get()) } includes(networkModule(isDebug)) } \ No newline at end of file diff --git a/core/data/src/main/java/org/mozilla/social/core/data/repository/MediaRepository.kt b/core/data/src/main/java/org/mozilla/social/core/data/repository/MediaRepository.kt index 6dd36e616..73e3a3a0a 100644 --- a/core/data/src/main/java/org/mozilla/social/core/data/repository/MediaRepository.kt +++ b/core/data/src/main/java/org/mozilla/social/core/data/repository/MediaRepository.kt @@ -5,7 +5,6 @@ import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody import org.mozilla.social.core.data.repository.model.status.toExternalModel import org.mozilla.social.core.network.MediaApi -import org.mozilla.social.core.network.model.request.NetworkMediaUpdate import org.mozilla.social.model.Attachment import java.io.File @@ -24,9 +23,4 @@ class MediaRepository internal constructor( ), description, ).toExternalModel() - - suspend fun updateMedia( - mediaId: String, - description: String, - ) = mediaApi.updateMedia(mediaId, NetworkMediaUpdate(description)) } \ No newline at end of file diff --git a/core/data/src/main/java/org/mozilla/social/core/data/repository/ReportRepository.kt b/core/data/src/main/java/org/mozilla/social/core/data/repository/ReportRepository.kt deleted file mode 100644 index 0c0f4bfbf..000000000 --- a/core/data/src/main/java/org/mozilla/social/core/data/repository/ReportRepository.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.mozilla.social.core.data.repository - -import org.mozilla.social.core.network.ReportApi -import org.mozilla.social.core.network.model.request.NetworkReportCreate - -class ReportRepository( - private val reportApi: ReportApi, -) { - - suspend fun report( - accountId: String, - statusIds: List? = null, - comment: String? = null, - forward: Boolean? = null, - category: String? = null, - ruleViolations: List? = null, - ) = reportApi.report( - body = NetworkReportCreate( - accountId = accountId, - statusIds = statusIds, - comment = comment, - forward = forward, - category = category, - ruleViolations = ruleViolations - ) - ) -} \ No newline at end of file diff --git a/core/data/src/main/java/org/mozilla/social/core/data/repository/TimelineRepository.kt b/core/data/src/main/java/org/mozilla/social/core/data/repository/TimelineRepository.kt index 37e8d1eef..91a007472 100644 --- a/core/data/src/main/java/org/mozilla/social/core/data/repository/TimelineRepository.kt +++ b/core/data/src/main/java/org/mozilla/social/core/data/repository/TimelineRepository.kt @@ -129,5 +129,4 @@ class TimelineRepository internal constructor( ) } } - } diff --git a/core/domain/src/main/java/org/mozilla/social/core/domain/DomainModule.kt b/core/domain/src/main/java/org/mozilla/social/core/domain/DomainModule.kt index daaff9bf8..1398ed0d6 100644 --- a/core/domain/src/main/java/org/mozilla/social/core/domain/DomainModule.kt +++ b/core/domain/src/main/java/org/mozilla/social/core/domain/DomainModule.kt @@ -10,6 +10,7 @@ import org.mozilla.social.core.domain.account.UnfollowAccount import org.mozilla.social.core.domain.account.UnmuteAccount import org.mozilla.social.core.domain.account.UpdateMyAccount import org.mozilla.social.core.domain.remotemediators.HashTagTimelineRemoteMediator +import org.mozilla.social.core.domain.report.Report val domainModule = module { factory { parametersHolder -> @@ -77,4 +78,9 @@ val domainModule = module { accountApi = get(), socialDatabase = get(), ) } + single { Report( + externalScope = get(), + showSnackbar = get(), + reportApi = get(), + ) } } \ No newline at end of file diff --git a/core/domain/src/main/java/org/mozilla/social/core/domain/report/Report.kt b/core/domain/src/main/java/org/mozilla/social/core/domain/report/Report.kt new file mode 100644 index 000000000..45c013101 --- /dev/null +++ b/core/domain/src/main/java/org/mozilla/social/core/domain/report/Report.kt @@ -0,0 +1,52 @@ +package org.mozilla.social.core.domain.report + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import org.mozilla.social.common.utils.StringFactory +import org.mozilla.social.core.domain.R +import org.mozilla.social.core.navigation.usecases.ShowSnackbar +import org.mozilla.social.core.network.ReportApi +import org.mozilla.social.core.network.model.request.NetworkReportCreate + +class Report( + private val externalScope: CoroutineScope, + private val showSnackbar: ShowSnackbar, + private val reportApi: ReportApi, + private val dispatcherIo: CoroutineDispatcher = Dispatchers.IO, +) { + + /** + * @throws ReportFailedException if any error occurred + */ + suspend operator fun invoke( + accountId: String, + statusIds: List? = null, + comment: String? = null, + forward: Boolean? = null, + category: String? = null, + ruleViolations: List? = null, + ) = externalScope.async(dispatcherIo) { + try { + reportApi.report( + body = NetworkReportCreate( + accountId = accountId, + statusIds = statusIds, + comment = comment, + forward = forward, + category = category, + ruleViolations = ruleViolations + ) + ) + } catch (e: Exception) { + showSnackbar( + text = StringFactory.resource(R.string.error_sending_report_toast), + isError = true, + ) + throw ReportFailedException(e) + } + }.await() + + class ReportFailedException(e: Exception) : Exception(e) +} \ No newline at end of file diff --git a/core/domain/src/main/res/values/strings.xml b/core/domain/src/main/res/values/strings.xml index a53b7ae74..c9f940442 100644 --- a/core/domain/src/main/res/values/strings.xml +++ b/core/domain/src/main/res/values/strings.xml @@ -12,6 +12,7 @@ Error following account Error unfollowing account Error deleting post + Error sending report Failed to save changes \ No newline at end of file diff --git a/core/domain/src/test/java/org/mozilla/social/core/domain/BaseDomainTest.kt b/core/domain/src/test/java/org/mozilla/social/core/domain/BaseDomainTest.kt index d3f1b6d03..7bec9f90f 100644 --- a/core/domain/src/test/java/org/mozilla/social/core/domain/BaseDomainTest.kt +++ b/core/domain/src/test/java/org/mozilla/social/core/domain/BaseDomainTest.kt @@ -6,6 +6,7 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -104,39 +105,31 @@ open class BaseDomainTest { subjectCallBlock: suspend () -> Unit, verifyBlock: suspend MockKVerificationScope.() -> Unit, ) = runTest { - val mutex1 = Mutex(locked = true) - val mutex2 = Mutex(locked = true) + val waitToCancel = CompletableDeferred() + val waitToFinish = CompletableDeferred() coEvery(delayedCallBlock).coAnswers { - CoroutineScope(Dispatchers.Default).launch { - // we know we've started the subject block, so we can unlock the first mutex - mutex1.unlock() - println("lock 1 unlocked") - delay(100) - // unlock to allow the verify block to run - mutex2.unlock() - println("lock 2 unlocked") - }.join() + println("allow cancel") + waitToCancel.complete(Unit) + println("wait to finish") + waitToFinish.await() + println("finishing subject block") delayedCallBlockReturnValue } - val outerJob = CoroutineScope(Dispatchers.Default).launch { + val outerJob = launch { subjectCallBlock() } - // lock to make sure we give the subject callback time to start - println("lock 1 locking") - mutex1.lock() + println("wait to cancel") + waitToCancel.await() + println("canceling outer job") outerJob.cancel() + println("allow finish") + waitToFinish.complete(Unit) - // lock again to make sure our delayed callback has run before we verify - println("lock 2 locking") - mutex2.lock() - - // delay to make sure the rest of the subject block has had time to run - delay(50) - + println("verify") coVerify(exactly = 1, verifyBlock = verifyBlock) } @@ -149,23 +142,19 @@ open class BaseDomainTest { delayedCallBlock: suspend MockKMatcherScope.() -> Any, subjectCallBlock: suspend () -> Unit, ) = runTest { - val mutex1 = Mutex(locked = true) - val mutex2 = Mutex(locked = true) + val waitToCancel = CompletableDeferred() + val waitToFinish = CompletableDeferred() coEvery(delayedCallBlock).coAnswers { - CoroutineScope(Dispatchers.Default).async { - // we know we've started the subject block, so we can unlock the first mutex - mutex1.unlock() - println("lock 1 unlocked") - delay(100) - // unlock to allow the verify block to run - mutex2.unlock() - println("lock 2 unlocked") - throw TestException() - }.await() + println("allow cancel") + waitToCancel.complete(Unit) + println("wait to finish") + waitToFinish.await() + println("finishing subject block") + throw TestException() } - val outerJob = CoroutineScope(Dispatchers.Default).launch { + val outerJob = launch { try { subjectCallBlock() } catch (e: TestException) { @@ -175,19 +164,12 @@ open class BaseDomainTest { } } - // lock to make sure we give the subject callback time to start - println("lock 1 locking") - mutex1.lock() - + println("wait to cancel") + waitToCancel.await() + println("canceling outer job") outerJob.cancel() - - // lock again to make sure our delayed callback has run before we verify - println("lock 2 locking") - mutex2.lock() - - // delay to make sure the rest of the subject block has had time to run - delay(50) - println("test ending") + println("allow finish") + waitToFinish.complete(Unit) } } diff --git a/core/domain/src/test/java/org/mozilla/social/core/domain/report/ReportTest.kt b/core/domain/src/test/java/org/mozilla/social/core/domain/report/ReportTest.kt new file mode 100644 index 000000000..44e32176e --- /dev/null +++ b/core/domain/src/test/java/org/mozilla/social/core/domain/report/ReportTest.kt @@ -0,0 +1,49 @@ +package org.mozilla.social.core.domain.report + +import kotlinx.coroutines.test.TestScope +import org.mozilla.social.core.domain.BaseDomainTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class ReportTest : BaseDomainTest() { + + private lateinit var subject: Report + + @BeforeTest + fun setup() { + subject = Report( + externalScope = TestScope(testDispatcher), + showSnackbar = showSnackbar, + reportApi = reportApi, + dispatcherIo = testDispatcher, + ) + } + + @Test + fun testCancelledScope() { + val accountId = "id1" + testOuterScopeCancelled( + delayedCallBlock = { + reportApi.report(any()) + }, + subjectCallBlock = { + subject(accountId) + }, + verifyBlock = { + reportApi.report(any()) + } + ) + } + + @Test + fun testCancelledScopeWithError() { + testOuterScopeCancelledAndInnerException( + delayedCallBlock = { + reportApi.report(any()) + }, + subjectCallBlock = { + subject("id") + }, + ) + } +} \ No newline at end of file diff --git a/feature/report/src/main/java/org/mozilla/social/feature/report/ReportModule.kt b/feature/report/src/main/java/org/mozilla/social/feature/report/ReportModule.kt index d28a227a0..da15896e6 100644 --- a/feature/report/src/main/java/org/mozilla/social/feature/report/ReportModule.kt +++ b/feature/report/src/main/java/org/mozilla/social/feature/report/ReportModule.kt @@ -20,18 +20,17 @@ val reportModule = module { } viewModel { parametersHolder -> ReportScreen2ViewModel( - get(), - get(), - get(), - parametersHolder[0], - parametersHolder[1], - parametersHolder[2], - parametersHolder[3], - parametersHolder[4], - parametersHolder[5], - parametersHolder[6], - parametersHolder[7], - parametersHolder[8], + report = get(), + accountRepository = get(), + onClose = parametersHolder[0], + onReportSubmitted = parametersHolder[1], + reportAccountId = parametersHolder[2], + reportAccountHandle = parametersHolder[3], + reportStatusId = parametersHolder[4], + reportType = parametersHolder[5], + checkedInstanceRules = parametersHolder[6], + additionalText = parametersHolder[7], + sendToExternalServer = parametersHolder[8], ) } viewModel { parametersHolder -> diff --git a/feature/report/src/main/java/org/mozilla/social/feature/report/step2/ReportScreen2ViewModel.kt b/feature/report/src/main/java/org/mozilla/social/feature/report/step2/ReportScreen2ViewModel.kt index 38a6d2962..20f592ce1 100644 --- a/feature/report/src/main/java/org/mozilla/social/feature/report/step2/ReportScreen2ViewModel.kt +++ b/feature/report/src/main/java/org/mozilla/social/feature/report/step2/ReportScreen2ViewModel.kt @@ -2,29 +2,21 @@ package org.mozilla.social.feature.report.step2 import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.mozilla.social.common.Resource -import org.mozilla.social.common.utils.StringFactory -import org.mozilla.social.common.utils.edit import org.mozilla.social.core.data.repository.AccountRepository -import org.mozilla.social.core.data.repository.ReportRepository -import org.mozilla.social.core.navigation.usecases.ShowSnackbar -import org.mozilla.social.feature.report.R +import org.mozilla.social.core.domain.report.Report import org.mozilla.social.feature.report.ReportDataBundle import org.mozilla.social.feature.report.ReportType import org.mozilla.social.model.InstanceRule import timber.log.Timber class ReportScreen2ViewModel( + private val report: Report, private val accountRepository: AccountRepository, - private val reportRepository: ReportRepository, - private val showSnackbar: ShowSnackbar, private val onClose: () -> Unit, private val onReportSubmitted: (bundle: ReportDataBundle.ReportDataBundleForScreen3) -> Unit, private val reportAccountId: String, @@ -73,7 +65,7 @@ class ReportScreen2ViewModel( _reportIsSending.update { true } viewModelScope.launch { try { - reportRepository.report( + report( accountId = reportAccountId, statusIds = buildList { reportStatusId?.let { add(it) } @@ -93,12 +85,8 @@ class ReportScreen2ViewModel( didUserReportAccount = true, ) ) - } catch (e: Exception) { + } catch (e: Report.ReportFailedException) { Timber.e(e) - showSnackbar( - text = StringFactory.resource(R.string.error_sending_report_toast), - isError = true, - ) _reportIsSending.update { false } } }