Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Tenor support #11587

Merged
Merged
Changes from 1 commit
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
06f49e1
Added Tenor library and API key reference
ThomazFB Mar 21, 2020
be9e118
Changed giphy package name to gifs
ThomazFB Mar 22, 2020
47ac89a
Created TenorProvider infrastructure and basic search
ThomazFB Mar 22, 2020
99751e6
Added GifProvider and TenorProvider documentation
ThomazFB Mar 23, 2020
d393dca
GifProvider is now available through injection with Dagger and provid…
ThomazFB Mar 23, 2020
c401c4c
Replaced Giphy name to Gif from all packages and classes
ThomazFB Mar 23, 2020
6554d88
Connected the TenorProvider implementation to the expected GifMediaVi…
ThomazFB Mar 23, 2020
e376715
Improved test suite for TenorProvider with code refactor and more tes…
ThomazFB Mar 23, 2020
b2d093f
Improved code reuse, naming and deprecated assertion methods
ThomazFB Mar 23, 2020
2e06912
Gif data class removed, using the MutableGifMediaViewModel instead. A…
ThomazFB Mar 24, 2020
bddff06
Fixed GiphyPickerActivity reference on AndroidManifest
ThomazFB Mar 24, 2020
a75cd47
Integrated pagination into the Tenor requests
ThomazFB Mar 24, 2020
91a00b2
Reverted Giphy renaming
ThomazFB Mar 24, 2020
7e384e3
Reverted Giphy renaming for packages
ThomazFB Mar 24, 2020
5b1f835
Additional code refactoring and new test for TenorProvider
ThomazFB Mar 24, 2020
0d23a81
Reconnected GIF search with MediaBrowserActivity
ThomazFB Mar 24, 2020
aca0ca2
Initial load changed to 42
ThomazFB Mar 24, 2020
2e46c33
Improved documentation for GifProvider
ThomazFB Mar 25, 2020
14da4b2
Added code refactoring for parameter organization
ThomazFB Mar 25, 2020
d338de2
Removed unwanted Tenor text reference
ThomazFB Mar 25, 2020
f664593
Added code refactoring for TenorProvider
ThomazFB Mar 25, 2020
3289614
Improved documentation over TenorProvider methods
ThomazFB Mar 25, 2020
f267c9a
Added resilience to the initial load fetching
ThomazFB Mar 25, 2020
40f781c
Changed constant to better fit code convention
ThomazFB Mar 25, 2020
64733fc
Changed TenorProviderTestUtils to TenorProviderTestFixtures, also ref…
ThomazFB Mar 25, 2020
6561720
Changed test assertion to use assertThat pattern and improve test rea…
ThomazFB Mar 25, 2020
a69afcc
Moved Tenor API initialization to the outside of TenorProvider
ThomazFB Mar 25, 2020
45e70dd
Extracted magic numbers to constants
ThomazFB Mar 25, 2020
ec760a1
Reconnected EditPostActivity with GIF Picker
ThomazFB Mar 26, 2020
18f0c2e
Added tests for GIF Picker DataSource
ThomazFB Mar 26, 2020
f0f2006
Added thread safety and timeout handling to TenorProvider
ThomazFB Mar 27, 2020
6dbeea0
Restored Analytics tracking
ThomazFB Mar 27, 2020
ffe47e4
Added Tenor attribution icon, removed the Giphy one
ThomazFB Mar 27, 2020
e4c91bb
Fixed Picker ViewModel tests
ThomazFB Mar 27, 2020
c673a53
Formatted tenor image
ThomazFB Mar 27, 2020
e3efa24
Refactored TenorProviderTest boilerplate
ThomazFB Mar 27, 2020
1a62bb1
Removed unnecessary methods for response and error handling
ThomazFB Mar 27, 2020
a854c91
Replaced Tenor callback implementation to the default retrofit one
ThomazFB Mar 27, 2020
4efed5f
Removed custom timeout exception implementation
ThomazFB Mar 27, 2020
ec38306
Removed coroutine scope injection
ThomazFB Mar 27, 2020
bbf6419
Remove unecessary annotation
ThomazFB Mar 31, 2020
40a5873
Renamed all giphy name references inside viewmodel package to only gif
ThomazFB Mar 29, 2020
3a41fcb
Renamed all giphy name references inside ui package to only gif
ThomazFB Mar 29, 2020
b5b0af6
Replaced all Giphy string references to Tenor
ThomazFB Mar 29, 2020
0ed1483
Adjusted all remaining documentation and code referencing Giphy
ThomazFB Mar 29, 2020
af0aaa7
Adjust GifMediaViewModel documentation
ThomazFB Apr 1, 2020
35f8d0a
Removed maven reference for giphy SDK
ThomazFB Apr 1, 2020
93fe31c
Removed translations of gif strings
ThomazFB Apr 1, 2020
a27b47a
Added local tenor feature flag through BuildConfig
ThomazFB Apr 2, 2020
dfc607f
Added Tenor selection availability through BuildConfig inside EditPos…
ThomazFB Apr 2, 2020
4a0cc13
Added Tenor selection availability through BuildConfig inside MediaBr…
ThomazFB Apr 2, 2020
d512243
Added the Tenor availability to the alpha build
ThomazFB Apr 2, 2020
41cdfaf
Added Tenor library license attribution
ThomazFB Apr 3, 2020
f3bf4e3
Fixed checkstyle errors
ThomazFB Apr 4, 2020
2ad832a
Replaced UI string usage for Exception for internal constant string
ThomazFB Apr 4, 2020
ceac897
Removed UI string usage from the exception for empty search result in…
ThomazFB Apr 4, 2020
0c09c88
Revert TenorProvider string usage with lint unused suppression
ThomazFB Apr 7, 2020
13e88ec
Changed the unused resources suppression declaration
ThomazFB Apr 7, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Reverted Giphy renaming
ThomazFB committed Apr 7, 2020

Verified

This commit was signed with the committer’s verified signature.
GuySartorelli Guy Sartorelli
commit 91a00b276d9cdebb2a6646eaddce73212f238a59
2 changes: 1 addition & 1 deletion WordPress/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -529,7 +529,7 @@
android:label=""
android:theme="@style/WordPress.NoActionBar" />
<activity
android:name=".ui.gifs.GiphyPickerActivity"
android:name=".ui.giphy.GiphyPickerActivity"
android:theme="@style/WordPress.NoActionBar" />

<!-- Notifications activities -->
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@
import org.wordpress.android.ui.domains.DomainRegistrationActivity;
import org.wordpress.android.ui.domains.DomainRegistrationDetailsFragment;
import org.wordpress.android.ui.domains.DomainSuggestionsFragment;
import org.wordpress.android.ui.gifs.GiphyPickerActivity;
import org.wordpress.android.ui.giphy.GiphyPickerActivity;
import org.wordpress.android.ui.history.HistoryAdapter;
import org.wordpress.android.ui.history.HistoryDetailContainerFragment;
import org.wordpress.android.ui.main.AddContentAdapter;
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@
import org.wordpress.android.viewmodel.activitylog.ActivityLogViewModel;
import org.wordpress.android.viewmodel.domains.DomainRegistrationDetailsViewModel;
import org.wordpress.android.viewmodel.domains.DomainSuggestionsViewModel;
import org.wordpress.android.viewmodel.gifs.GifPickerViewModel;
import org.wordpress.android.viewmodel.gifs.GiphyPickerViewModel;
import org.wordpress.android.viewmodel.history.HistoryViewModel;
import org.wordpress.android.viewmodel.main.WPMainActivityViewModel;
import org.wordpress.android.viewmodel.pages.PageListViewModel;
@@ -218,8 +218,8 @@ abstract class ViewModelModule {

@Binds
@IntoMap
@ViewModelKey(GifPickerViewModel.class)
abstract ViewModel giphyPickerViewModel(GifPickerViewModel viewModel);
@ViewModelKey(GiphyPickerViewModel.class)
abstract ViewModel giphyPickerViewModel(GiphyPickerViewModel viewModel);

@Binds
@IntoMap
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.wordpress.android.ui.gifs
package org.wordpress.android.ui.giphy

import android.view.LayoutInflater
import android.view.View
@@ -14,14 +14,14 @@ import org.wordpress.android.util.getDistinct
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.util.image.ImageType.PHOTO
import org.wordpress.android.util.redirectContextClickToLongPressListener
import org.wordpress.android.viewmodel.gifs.GifMediaViewModel
import org.wordpress.android.viewmodel.gifs.GiphyMediaViewModel

/**
* Represents a single item in the [GiphyPickerActivity]'s grid (RecyclerView).
*
* This is meant to show a single animated gif.
*
* This ViewHolder references a readonly [GifMediaViewModel]. It should never update the [GifMediaViewModel]. That
* This ViewHolder references a readonly [GiphyMediaViewModel]. It should never update the [GiphyMediaViewModel]. That
* behavior is handled by the [GiphyPickerViewModel]. This is designed this way so that [GiphyPickerViewModel]
* encapsulates all the logic of managing selected items as well as keeping their selection numbers continuous.
*/
@@ -35,11 +35,11 @@ class GiphyMediaViewHolder(
*
* If there is no bound [mediaViewModel], this can mean that there was an API error or this is just a placeholder.
*/
private val onClickListener: (GifMediaViewModel?) -> Unit,
private val onClickListener: (GiphyMediaViewModel?) -> Unit,
/**
* A function that is called when the user performs a long press on the thumbnail
*/
private val onLongClickListener: (GifMediaViewModel) -> Unit,
private val onLongClickListener: (GiphyMediaViewModel) -> Unit,
/**
* The view used for this `ViewHolder`.
*/
@@ -48,13 +48,13 @@ class GiphyMediaViewHolder(
* The dimensions used for the ImageView
*/
thumbnailViewDimensions: ThumbnailViewDimensions
) : LifecycleOwnerViewHolder<GifMediaViewModel>(itemView) {
) : LifecycleOwnerViewHolder<GiphyMediaViewModel>(itemView) {
data class ThumbnailViewDimensions(val width: Int, val height: Int)

private val thumbnailView: ImageView = itemView.image_thumbnail
private val selectionNumberTextView: TextView = itemView.text_selection_count

private var mediaViewModel: GifMediaViewModel? = null
private var mediaViewModel: GiphyMediaViewModel? = null

init {
thumbnailView.apply {
@@ -72,13 +72,13 @@ class GiphyMediaViewHolder(
}

/**
* Update the views to use the given [GifMediaViewModel]
* Update the views to use the given [GiphyMediaViewModel]
*
* The [mediaViewModel] is optional because we enable placeholders in the paged list created by
* [org.wordpress.android.viewmodel.gifs.GifPickerViewModel]. This causes null values to be bound to
* [org.wordpress.android.viewmodel.gifs.GiphyPickerViewModel]. This causes null values to be bound to
* [GiphyMediaViewHolder] instances.
*/
override fun bind(item: GifMediaViewModel?) {
override fun bind(item: GiphyMediaViewModel?) {
super.bind(item)

this.mediaViewModel = item
@@ -141,8 +141,8 @@ class GiphyMediaViewHolder(
*/
fun create(
imageManager: ImageManager,
onClickListener: (GifMediaViewModel?) -> Unit,
onLongClickListener: (GifMediaViewModel) -> Unit,
onClickListener: (GiphyMediaViewModel?) -> Unit,
onLongClickListener: (GiphyMediaViewModel) -> Unit,
parent: ViewGroup,
thumbnailViewDimensions: ThumbnailViewDimensions
): GiphyMediaViewHolder {
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.wordpress.android.ui.gifs
package org.wordpress.android.ui.giphy

import android.app.Activity
import android.content.Intent
@@ -19,19 +19,19 @@ import org.wordpress.android.WordPress
import org.wordpress.android.analytics.AnalyticsTracker
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.ui.ActionableEmptyView
import org.wordpress.android.ui.giphy.GiphyMediaViewHolder.ThumbnailViewDimensions
import org.wordpress.android.ui.LocaleAwareActivity
import org.wordpress.android.ui.gifs.GiphyMediaViewHolder.ThumbnailViewDimensions
import org.wordpress.android.ui.media.MediaPreviewActivity
import org.wordpress.android.util.AniUtils
import org.wordpress.android.util.DisplayUtils
import org.wordpress.android.util.ToastUtils
import org.wordpress.android.util.getDistinct
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.viewmodel.ViewModelFactory
import org.wordpress.android.viewmodel.gifs.GifMediaViewModel
import org.wordpress.android.viewmodel.gifs.GifPickerViewModel
import org.wordpress.android.viewmodel.gifs.GifPickerViewModel.EmptyDisplayMode
import org.wordpress.android.viewmodel.gifs.GifPickerViewModel.State
import org.wordpress.android.viewmodel.gifs.GiphyMediaViewModel
import org.wordpress.android.viewmodel.gifs.GiphyPickerViewModel
import org.wordpress.android.viewmodel.gifs.GiphyPickerViewModel.EmptyDisplayMode
import org.wordpress.android.viewmodel.gifs.GiphyPickerViewModel.State
import javax.inject.Inject

/**
@@ -46,7 +46,7 @@ class GiphyPickerActivity : LocaleAwareActivity() {
@Inject lateinit var imageManager: ImageManager
@Inject lateinit var viewModelFactory: ViewModelFactory

private lateinit var viewModel: GifPickerViewModel
private lateinit var viewModel: GiphyPickerViewModel

private val gridColumnCount: Int by lazy { if (DisplayUtils.isLandscape(this)) 4 else 3 }

@@ -64,7 +64,7 @@ class GiphyPickerActivity : LocaleAwareActivity() {

val site = intent.getSerializableExtra(WordPress.SITE) as SiteModel

viewModel = ViewModelProviders.of(this, viewModelFactory).get(GifPickerViewModel::class.java)
viewModel = ViewModelProviders.of(this, viewModelFactory).get(GiphyPickerViewModel::class.java)
viewModel.setup(site)

// We are intentionally reusing this layout since the UI is very similar.
@@ -150,7 +150,7 @@ class GiphyPickerActivity : LocaleAwareActivity() {
}

/**
* Configure the selection bar and its labels when the [GifPickerViewModel] selected items change
* Configure the selection bar and its labels when the [GiphyPickerViewModel] selected items change
*/
private fun initializeSelectionBar() {
viewModel.selectionBarIsVisible.observe(this, Observer {
@@ -275,7 +275,7 @@ class GiphyPickerActivity : LocaleAwareActivity() {
*
* @param mediaViewModels A non-empty list
*/
private fun showPreview(mediaViewModels: List<GifMediaViewModel>) {
private fun showPreview(mediaViewModels: List<GiphyMediaViewModel>) {
check(mediaViewModels.isNotEmpty())

val uris = mediaViewModels.map { it.previewImageUri.toString() }
@@ -308,7 +308,7 @@ class GiphyPickerActivity : LocaleAwareActivity() {
}

/**
* Set up enabling/disabling of controls depending on the current [GifPickerViewModel.State]:
* Set up enabling/disabling of controls depending on the current [GiphyPickerViewModel.State]:
*
* - [State.IDLE]: All normal functions are allowed
* - [State.DOWNLOADING] or [State.FINISHED]: "Add", "Preview", searching, and selecting are disabled
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package org.wordpress.android.ui.gifs
package org.wordpress.android.ui.giphy

import android.view.ViewGroup
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil.ItemCallback
import org.wordpress.android.ui.gifs.GiphyMediaViewHolder.ThumbnailViewDimensions
import org.wordpress.android.ui.giphy.GiphyMediaViewHolder.ThumbnailViewDimensions
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.viewmodel.gifs.GifMediaViewModel
import org.wordpress.android.viewmodel.gifs.GiphyMediaViewModel

/**
* An [RecyclerView] adapter to be used with the [PagedList] created by [GiphyPickerViewModel]
*/
class GiphyPickerPagedListAdapter(
private val imageManager: ImageManager,
private val thumbnailViewDimensions: ThumbnailViewDimensions,
private val onMediaViewClickListener: (GifMediaViewModel?) -> Unit,
private val onMediaViewLongClickListener: (GifMediaViewModel) -> Unit
) : PagedListAdapter<GifMediaViewModel, GiphyMediaViewHolder>(DIFF_CALLBACK) {
private val onMediaViewClickListener: (GiphyMediaViewModel?) -> Unit,
private val onMediaViewLongClickListener: (GiphyMediaViewModel) -> Unit
) : PagedListAdapter<GiphyMediaViewModel, GiphyMediaViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GiphyMediaViewHolder {
return GiphyMediaViewHolder.create(
imageManager = imageManager,
@@ -31,18 +31,18 @@ class GiphyPickerPagedListAdapter(
}

companion object {
private val DIFF_CALLBACK = object : ItemCallback<GifMediaViewModel>() {
override fun areItemsTheSame(oldItem: GifMediaViewModel, newItem: GifMediaViewModel): Boolean {
private val DIFF_CALLBACK = object : ItemCallback<GiphyMediaViewModel>() {
override fun areItemsTheSame(oldItem: GiphyMediaViewModel, newItem: GiphyMediaViewModel): Boolean {
return oldItem.id == newItem.id
}

/**
* Always assume that two similar [GifMediaViewModel] objects always have the same content.
* Always assume that two similar [GiphyMediaViewModel] objects always have the same content.
*
* It is probably extremely unlikely that GIFs from Giphy will change while the user is performing
* a search.
*/
override fun areContentsTheSame(oldItem: GifMediaViewModel, newItem: GifMediaViewModel): Boolean {
override fun areContentsTheSame(oldItem: GiphyMediaViewModel, newItem: GiphyMediaViewModel): Boolean {
return true
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.wordpress.android.ui.gifs
package org.wordpress.android.ui.giphy

import android.view.View
import android.view.View.OnAttachStateChangeListener
Original file line number Diff line number Diff line change
@@ -18,13 +18,13 @@ import org.wordpress.android.util.WPMediaUtils
import javax.inject.Inject

/**
* Downloads [GifMediaViewModel.largeImageUri] objects and saves them as [MediaModel]
* Downloads [GiphyMediaViewModel.largeImageUri] objects and saves them as [MediaModel]
*
* The download happens concurrently and primarily uses [Dispatchers.IO]. This means that we are limited by the number
* of threads backed by that `CoroutineDispatcher`. In the future, we should probably add a limit to the number of
* GIFs that users can select to reduce the likelihood of OOM exceptions.
*/
class GifMediaFetcher @Inject constructor(
class GiphyMediaFetcher @Inject constructor(
private val context: Context,
private val mediaStore: MediaStore,
private val dispatcher: Dispatcher
@@ -38,22 +38,22 @@ class GifMediaFetcher @Inject constructor(
*/
@Throws
suspend fun fetchAndSave(
gifMediaViewModels: List<GifMediaViewModel>,
giphyMediaViewModels: List<GiphyMediaViewModel>,
site: SiteModel
): List<MediaModel> = coroutineScope {
// Execute [fetchAndSave] for all giphyMediaViewModels first so that they are queued and executed in the
// background. We'll call `await()` once they are queued.
return@coroutineScope gifMediaViewModels.map {
fetchAndSave(scope = this, gifMediaViewModel = it, site = site)
return@coroutineScope giphyMediaViewModels.map {
fetchAndSave(scope = this, giphyMediaViewModel = it, site = site)
}.map { it.await() }
}

private fun fetchAndSave(
scope: CoroutineScope,
gifMediaViewModel: GifMediaViewModel,
giphyMediaViewModel: GiphyMediaViewModel,
site: SiteModel
): Deferred<MediaModel> = scope.async(Dispatchers.IO) {
val uri = gifMediaViewModel.largeImageUri
val uri = giphyMediaViewModel.largeImageUri
// No need to log the Exception here. The underlying method that is used, [MediaUtils.downloadExternalMedia]
// already logs any errors.
val downloadedUri = WPMediaUtils.fetchMedia(context, uri) ?: throw Exception("Failed to download the image.")
@@ -65,7 +65,7 @@ class GifMediaFetcher @Inject constructor(
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension)

val mediaModel = FluxCUtils.mediaModelFromLocalUri(context, downloadedUri, mimeType, mediaStore, site.id)
mediaModel.title = gifMediaViewModel.title
mediaModel.title = giphyMediaViewModel.title
dispatcher.dispatch(MediaActionBuilder.newUpdateMediaAction(mediaModel))

return@async mediaModel
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ import androidx.lifecycle.LiveData
* See the [Giphy API docs](https://developers.giphy.com/docs/) for more information on what a [Media] object contains.
* Search for "The GIF Object" section.
*/
interface GifMediaViewModel {
interface GiphyMediaViewModel {
/**
* The id from Giphy's [Media]
*/
Original file line number Diff line number Diff line change
@@ -6,28 +6,28 @@ import androidx.paging.PositionalDataSource
import org.wordpress.android.viewmodel.gifs.provider.GifProvider

/**
* The PagedListDataSource that is created and managed by [GifPickerDataSourceFactory]
* The PagedListDataSource that is created and managed by [GiphyPickerDataSourceFactory]
*
* This performs paged API requests using the [apiClient]. A new instance of this class must be created if the
* [searchQuery] is changed by the user.
*/
class GifPickerDataSource(
class GiphyPickerDataSource(
private val gifProvider: GifProvider,
private val searchQuery: String
) : PositionalDataSource<GifMediaViewModel>() {
) : PositionalDataSource<GiphyMediaViewModel>() {
/**
* The data structure used for storing failed [loadRange] calls so they can be retried later.
*/
private data class RangeLoadArguments(
val params: LoadRangeParams,
val callback: LoadRangeCallback<GifMediaViewModel>
val callback: LoadRangeCallback<GiphyMediaViewModel>
)

/**
* The error received when [loadInitial] fails.
*
* Unlike [rangeLoadErrorEvent], this is not a [LiveData] because the consumer of this method
* [GifPickerViewModel] simply uses it to check for null values and reacts to a different event.
* [GiphyPickerViewModel] simply uses it to check for null values and reacts to a different event.
*
* This is cleared when [loadInitial] is started.
*/
@@ -53,15 +53,15 @@ class GifPickerDataSource(
/**
* Always the load the first page (startingPosition = 0) from the Giphy API
*
* The [GifPickerDataSourceFactory] recreates [GifPickerDataSource] instances whenever a new [searchQuery]
* The [GiphyPickerDataSourceFactory] recreates [GiphyPickerDataSource] instances whenever a new [searchQuery]
* is queued. The [LoadInitialParams.requestedStartPosition] may have a value that is only valid for the
* previous [searchQuery]. If that value is greater than the total search results of the new [searchQuery],
* a crash will happen.
*
* Using `0` as the `startPosition` forces the [GifPickerDataSource] consumer to reset the list (UI) from the
* Using `0` as the `startPosition` forces the [GiphyPickerDataSource] consumer to reset the list (UI) from the
* top.
*/
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<GifMediaViewModel>) {
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<GiphyMediaViewModel>) {
val startPosition = 0

initialLoadError = null
@@ -88,7 +88,7 @@ class GifPickerDataSource(
* Errors are dispatched to [rangeLoadErrorEvent]. If successful, previously failed calls of this method are
* automatically retried using [retryAllFailedRangeLoads].
*/
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<GifMediaViewModel>) {
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<GiphyMediaViewModel>) {
gifProvider.search(
searchQuery,
params.startPosition,
Original file line number Diff line number Diff line change
@@ -9,19 +9,19 @@ import org.wordpress.android.viewmodel.gifs.provider.GifProvider
import javax.inject.Inject

/**
* Creates instances of [GifPickerDataSource]
* Creates instances of [GiphyPickerDataSource]
*
* Whenever the [searchQuery] is changed:
*
* 1. The last [GifPickerDataSource] is invalidated
* 2. The [LivePagedListBuilder] will create a new [GifPickerDataSource] by calling [create]
* 3. The new [GifPickerDataSource] will start another paged API request
* 1. The last [GiphyPickerDataSource] is invalidated
* 2. The [LivePagedListBuilder] will create a new [GiphyPickerDataSource] by calling [create]
* 3. The new [GiphyPickerDataSource] will start another paged API request
*/
class GifPickerDataSourceFactory @Inject constructor(private val gifProvider: GifProvider) : Factory<Int, GifMediaViewModel>() {
class GiphyPickerDataSourceFactory @Inject constructor(private val gifProvider: GifProvider) : Factory<Int, GiphyMediaViewModel>() {
/**
* The active search query.
*
* When changed, the current [GifPickerDataSource] will be invalidated. A new API search will be performed.
* When changed, the current [GiphyPickerDataSource] will be invalidated. A new API search will be performed.
*/
var searchQuery: String = ""
set(value) {
@@ -34,26 +34,26 @@ class GifPickerDataSourceFactory @Inject constructor(private val gifProvider: Gi
*
* We retain this so we can invalidate it later when [searchQuery] is changed.
*/
private val dataSource = MutableLiveData<GifPickerDataSource>()
private val dataSource = MutableLiveData<GiphyPickerDataSource>()

/**
* The [GifPickerDataSource.initialLoadError] of the current [dataSource]
* The [GiphyPickerDataSource.initialLoadError] of the current [dataSource]
*/
val initialLoadError: Throwable? get() = dataSource.value?.initialLoadError
/**
* The [GifPickerDataSource.rangeLoadErrorEvent] of the current [dataSource]
* The [GiphyPickerDataSource.rangeLoadErrorEvent] of the current [dataSource]
*/
val rangeLoadErrorEvent: LiveData<Throwable> = Transformations.switchMap(dataSource) { it.rangeLoadErrorEvent }

/**
* Retries all previously failed page loads.
*
* @see [GifPickerDataSource.retryAllFailedRangeLoads]
* @see [GiphyPickerDataSource.retryAllFailedRangeLoads]
*/
fun retryAllFailedRangeLoads() = dataSource.value?.retryAllFailedRangeLoads()

override fun create(): DataSource<Int, GifMediaViewModel> {
val dataSource = GifPickerDataSource(gifProvider, searchQuery)
override fun create(): DataSource<Int, GiphyMediaViewModel> {
val dataSource = GiphyPickerDataSource(gifProvider, searchQuery)
this.dataSource.postValue(dataSource)
return dataSource
}
Original file line number Diff line number Diff line change
@@ -24,22 +24,22 @@ import org.wordpress.android.viewmodel.SingleLiveEvent
import javax.inject.Inject

/**
* Holds the data for [org.wordpress.android.ui.gifs.GiphyPickerActivity]
* Holds the data for [org.wordpress.android.ui.giphy.GiphyPickerActivity]
*
* This creates a [PagedList] which can be bound to by a [PagedListAdapter] and also manages the logic of the
* selected media. That includes but not limited to keeping the [GifMediaViewModel.selectionNumber] continuous.
* selected media. That includes but not limited to keeping the [GiphyMediaViewModel.selectionNumber] continuous.
*
* Calling [setup] is required before using this ViewModel.
*/
class GifPickerViewModel @Inject constructor(
class GiphyPickerViewModel @Inject constructor(
private val networkUtils: NetworkUtilsWrapper,
private val mediaFetcher: GifMediaFetcher,
private val mediaFetcher: GiphyMediaFetcher,
/**
* The [GifPickerDataSourceFactory] to use
* The [GiphyPickerDataSourceFactory] to use
*
* This is only available in the constructor to allow mocking in tests.
*/
private val dataSourceFactory: GifPickerDataSourceFactory
private val dataSourceFactory: GiphyPickerDataSourceFactory
) : CoroutineScopedViewModel() {
/**
* A result of [downloadSelected] observed using the [downloadResult] LiveData
@@ -95,7 +95,7 @@ class GifPickerViewModel @Inject constructor(
/**
* Errors that happened during page loads.
*
* @see [GifPickerDataSource.rangeLoadErrorEvent]
* @see [GiphyPickerDataSource.rangeLoadErrorEvent]
*/
val rangeLoadErrorEvent: LiveData<Throwable> = dataSourceFactory.rangeLoadErrorEvent

@@ -113,13 +113,13 @@ class GifPickerViewModel @Inject constructor(
*/
val downloadResult: LiveData<DownloadResult> = _downloadResult

private val _selectedMediaViewModelList = MutableLiveData<LinkedHashMap<String, GifMediaViewModel>>()
private val _selectedMediaViewModelList = MutableLiveData<LinkedHashMap<String, GiphyMediaViewModel>>()
/**
* A [Map] of the [GifMediaViewModel]s that were selected by the user
* A [Map] of the [GiphyMediaViewModel]s that were selected by the user
*
* This map is sorted in the order that the user picked them. The [String] is the value of [GifMediaViewModel.id].
* This map is sorted in the order that the user picked them. The [String] is the value of [GiphyMediaViewModel.id].
*/
val selectedMediaViewModelList: LiveData<LinkedHashMap<String, GifMediaViewModel>> = _selectedMediaViewModelList
val selectedMediaViewModelList: LiveData<LinkedHashMap<String, GiphyMediaViewModel>> = _selectedMediaViewModelList

/**
* Returns `true` if the selection bar (UI) should be shown
@@ -140,7 +140,7 @@ class GifPickerViewModel @Inject constructor(
/**
* The [PagedList] that should be displayed in the RecyclerView
*/
val mediaViewModelPagedList: LiveData<PagedList<GifMediaViewModel>> by lazy {
val mediaViewModelPagedList: LiveData<PagedList<GiphyMediaViewModel>> by lazy {
val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPrefetchDistance(15)
@@ -156,7 +156,7 @@ class GifPickerViewModel @Inject constructor(
/**
* Update the [emptyDisplayMode] depending on the number of API search results or whether there was an error.
*/
private val pagedListBoundaryCallback = object : BoundaryCallback<GifMediaViewModel>() {
private val pagedListBoundaryCallback = object : BoundaryCallback<GiphyMediaViewModel>() {
override fun onZeroItemsLoaded() {
_isPerformingInitialLoad.postValue(false)

@@ -169,7 +169,7 @@ class GifPickerViewModel @Inject constructor(
super.onZeroItemsLoaded()
}

override fun onItemAtFrontLoaded(itemAtFront: GifMediaViewModel) {
override fun onItemAtFrontLoaded(itemAtFront: GiphyMediaViewModel) {
_isPerformingInitialLoad.postValue(false)
_emptyDisplayMode.postValue(EmptyDisplayMode.HIDDEN)
super.onItemAtFrontLoaded(itemAtFront)
@@ -207,7 +207,7 @@ class GifPickerViewModel @Inject constructor(
* when, presumably, the user has stopped typing.
*
* This also clears the [selectedMediaViewModelList]. This makes sense because the user will not be seeing the
* currently selected [GifMediaViewModel] if the new search query results are different.
* currently selected [GiphyMediaViewModel] if the new search query results are different.
*
* Searching is disabled if downloading or the [query] is the same as the last one.
*
@@ -239,7 +239,7 @@ class GifPickerViewModel @Inject constructor(
}

/**
* Downloads all the selected [GifMediaViewModel]
* Downloads all the selected [GiphyMediaViewModel]
*
* When the process is finished, the results will be posted to [downloadResult].
*
@@ -284,17 +284,17 @@ class GifPickerViewModel @Inject constructor(
}

/**
* Toggles a [GifMediaViewModel]'s `isSelected` property between true and false
* Toggles a [GiphyMediaViewModel]'s `isSelected` property between true and false
*
* This also updates the [GifMediaViewModel.selectionNumber] of all the objects in [selectedMediaViewModelList].
* This also updates the [GiphyMediaViewModel.selectionNumber] of all the objects in [selectedMediaViewModelList].
*/
fun toggleSelected(mediaViewModel: GifMediaViewModel) {
fun toggleSelected(mediaViewModel: GiphyMediaViewModel) {
if (_state.value != State.IDLE) {
return
}

assert(mediaViewModel is MutableGifMediaViewModel)
mediaViewModel as MutableGifMediaViewModel
assert(mediaViewModel is MutableGiphyMediaViewModel)
mediaViewModel as MutableGiphyMediaViewModel

val isSelected = !(mediaViewModel.isSelected.value ?: false)

@@ -316,14 +316,14 @@ class GifPickerViewModel @Inject constructor(
}

/**
* Update the [GifMediaViewModel.selectionNumber] values so that they are continuous
* Update the [GiphyMediaViewModel.selectionNumber] values so that they are continuous
*
* For example, if the selection numbers are [1, 2, 3, 4, 5] and the 2nd [GifMediaViewModel] was removed, we
* For example, if the selection numbers are [1, 2, 3, 4, 5] and the 2nd [GiphyMediaViewModel] was removed, we
* want the selection numbers to be updated to [1, 2, 3, 4] instead of leaving it as [1, 3, 4, 5].
*/
private fun rebuildSelectionNumbers(mediaList: LinkedHashMap<String, GifMediaViewModel>) {
private fun rebuildSelectionNumbers(mediaList: LinkedHashMap<String, GiphyMediaViewModel>) {
mediaList.values.forEachIndexed { index, mediaViewModel ->
(mediaViewModel as MutableGifMediaViewModel).postSelectionNumber(index + 1)
(mediaViewModel as MutableGiphyMediaViewModel).postSelectionNumber(index + 1)
}
}

@@ -349,7 +349,7 @@ class GifPickerViewModel @Inject constructor(
/**
* Retries all previously failed page loads.
*
* @see [GifPickerDataSource.retryAllFailedRangeLoads]
* @see [GiphyPickerDataSource.retryAllFailedRangeLoads]
*/
fun retryAllFailedRangeLoads() = dataSourceFactory.retryAllFailedRangeLoads()
}
Original file line number Diff line number Diff line change
@@ -6,21 +6,21 @@ import androidx.lifecycle.MutableLiveData
import org.wordpress.android.viewmodel.SingleLiveEvent

/**
* A mutable implementation of [GifMediaViewModel]
* A mutable implementation of [GiphyMediaViewModel]
*
* This is meant to be accessible by [GifPickerViewModel] and [GifPickerDataSource] only. This is designed this
* way so that [GifPickerViewModel] encapsulates all the logic of managing selected items as well as keeping their
* This is meant to be accessible by [GiphyPickerViewModel] and [GiphyPickerDataSource] only. This is designed this
* way so that [GiphyPickerViewModel] encapsulates all the logic of managing selected items as well as keeping their
* selection numbers continuous.
*
* The [GiphyPickerViewHolder] should never have access to the mutating methods of this class.
*/
data class MutableGifMediaViewModel(
data class MutableGiphyMediaViewModel(
override val id: String,
override val thumbnailUri: Uri,
override val previewImageUri: Uri,
override val largeImageUri: Uri,
override val title: String
) : GifMediaViewModel {
) : GiphyMediaViewModel {
/**
* Using [SingleLiveEvent] will prevent calls like this from running immediately when a ViewHolder is bound:
*
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.wordpress.android.viewmodel.gifs.provider

import org.wordpress.android.viewmodel.gifs.GifMediaViewModel
import org.wordpress.android.viewmodel.gifs.GiphyMediaViewModel

/**
* Interface to interact with a GIF provider API avoiding coupling with the concrete implementation
@@ -13,7 +13,7 @@ interface GifProvider {
* [position] to request results starting from a given position for that [query]
* [loadSize] to request a result list limited to a specific amount
* [onSuccess] will be called if the Provider had success finding GIFs
* and will deliver a [List] of [GifMediaViewModel] with all matching GIFs
* and will deliver a [List] of [GiphyMediaViewModel] with all matching GIFs
* and a [Int] describing the next position for pagination
* [onFailure] will be called if the Provider didn't succeed with the task of bringing GIFs,
* the delivered String will describe the error to be presented to the user
@@ -22,7 +22,7 @@ interface GifProvider {
query: String,
position: Int,
loadSize: Int? = null,
onSuccess: (List<GifMediaViewModel>, Int?) -> Unit,
onSuccess: (List<GiphyMediaViewModel>, Int?) -> Unit,
onFailure: (Throwable) -> Unit
)

Original file line number Diff line number Diff line change
@@ -13,8 +13,8 @@ import com.tenor.android.core.response.WeakRefCallback
import com.tenor.android.core.response.impl.GifsResponse
import org.wordpress.android.BuildConfig
import org.wordpress.android.R.string
import org.wordpress.android.viewmodel.gifs.GifMediaViewModel
import org.wordpress.android.viewmodel.gifs.MutableGifMediaViewModel
import org.wordpress.android.viewmodel.gifs.GiphyMediaViewModel
import org.wordpress.android.viewmodel.gifs.MutableGiphyMediaViewModel
import org.wordpress.android.viewmodel.gifs.provider.GifProvider.GifRequestFailedException

/**
@@ -59,7 +59,7 @@ internal class TenorProvider @JvmOverloads constructor(
query: String,
position: Int,
loadSize: Int?,
onSuccess: (List<GifMediaViewModel>, Int?) -> Unit,
onSuccess: (List<GiphyMediaViewModel>, Int?) -> Unit,
onFailure: (Throwable) -> Unit
) {
apiClient.simpleSearch(
@@ -142,10 +142,10 @@ internal class TenorProvider @JvmOverloads constructor(

/**
* Every GIF returned by the Tenor will be available as [Result], to better interface
* with our app, it will be converted to [MutableGifMediaViewModel] to avoid any external
* with our app, it will be converted to [MutableGiphyMediaViewModel] to avoid any external
* coupling with the Tenor API
*/
private fun Result.toMutableGifMediaViewModel() = MutableGifMediaViewModel(
private fun Result.toMutableGifMediaViewModel() = MutableGiphyMediaViewModel(
id,
Uri.parse(urlFromCollectionFormat(MediaCollectionFormat.GIF_NANO)),
Uri.parse(urlFromCollectionFormat(MediaCollectionFormat.GIF_TINY)),
Original file line number Diff line number Diff line change
@@ -19,26 +19,26 @@ import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
import org.wordpress.android.fluxc.model.MediaModel
import org.wordpress.android.util.NetworkUtilsWrapper
import org.wordpress.android.viewmodel.gifs.GifPickerViewModel.EmptyDisplayMode
import org.wordpress.android.viewmodel.gifs.GifPickerViewModel.State
import org.wordpress.android.viewmodel.gifs.GiphyPickerViewModel.EmptyDisplayMode
import org.wordpress.android.viewmodel.gifs.GiphyPickerViewModel.State
import java.util.Random
import java.util.UUID

@RunWith(MockitoJUnitRunner::class)
class GifPickerViewModelTest {
class GiphyPickerViewModelTest {
@get:Rule
val rule = InstantTaskExecutorRule()

private lateinit var viewModel: GifPickerViewModel
private lateinit var viewModel: GiphyPickerViewModel

private val dataSourceFactory = mock<GifPickerDataSourceFactory>()
private val mediaFetcher = mock<GifMediaFetcher>()
private val dataSourceFactory = mock<GiphyPickerDataSourceFactory>()
private val mediaFetcher = mock<GiphyMediaFetcher>()

@Mock private lateinit var networkUtils: NetworkUtilsWrapper

@Before
fun setUp() {
viewModel = GifPickerViewModel(
viewModel = GiphyPickerViewModel(
dataSourceFactory = dataSourceFactory,
networkUtils = networkUtils,
mediaFetcher = mediaFetcher
@@ -147,12 +147,12 @@ class GifPickerViewModelTest {
@Test
fun `when search results are empty, the empty view should be visible and says there are no results`() {
// Arrange
val dataSource = mock<GifPickerDataSource>()
val dataSource = mock<GiphyPickerDataSource>()

whenever(dataSourceFactory.create()).thenReturn(dataSource)
whenever(dataSourceFactory.searchQuery).thenReturn("dummy")

val callbackCaptor = argumentCaptor<LoadInitialCallback<GifMediaViewModel>>()
val callbackCaptor = argumentCaptor<LoadInitialCallback<GiphyMediaViewModel>>()
doNothing().whenever(dataSource).loadInitial(any(), callbackCaptor.capture())

// Observe mediaViewModelPagedList so the DataSourceFactory will be activated and perform API requests
@@ -174,12 +174,12 @@ class GifPickerViewModelTest {
@Test
fun `when the initial load fails, the empty view should show a network error`() {
// Arrange
val dataSource = mock<GifPickerDataSource>()
val dataSource = mock<GiphyPickerDataSource>()

whenever(dataSourceFactory.create()).thenReturn(dataSource)
whenever(dataSourceFactory.initialLoadError).thenReturn(mock())

val callbackCaptor = argumentCaptor<LoadInitialCallback<GifMediaViewModel>>()
val callbackCaptor = argumentCaptor<LoadInitialCallback<GiphyMediaViewModel>>()
doNothing().whenever(dataSource).loadInitial(any(), callbackCaptor.capture())

// Observe mediaViewModelPagedList so the DataSourceFactory will be activated and perform API requests
@@ -313,7 +313,7 @@ class GifPickerViewModelTest {
id = Random().nextInt()
}

private fun createGiphyMediaViewModel() = MutableGifMediaViewModel(
private fun createGiphyMediaViewModel() = MutableGiphyMediaViewModel(
id = UUID.randomUUID().toString(),
thumbnailUri = mock(),
largeImageUri = mock(),
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import com.tenor.android.core.constant.MediaCollectionFormat
import com.tenor.android.core.model.impl.Media
import com.tenor.android.core.model.impl.MediaCollection
import com.tenor.android.core.model.impl.Result
import org.wordpress.android.viewmodel.gifs.MutableGifMediaViewModel
import org.wordpress.android.viewmodel.gifs.MutableGiphyMediaViewModel

class TenorProviderTestUtils {
companion object {
@@ -49,7 +49,7 @@ class TenorProviderTestUtils {
mock<Media>().apply { whenever(this.url).thenReturn(mockContent) }

private fun createExpectedGifMediaViewModel(expectedContent: String) =
MutableGifMediaViewModel(
MutableGiphyMediaViewModel(
expectedContent,
Uri.parse("$expectedContent gif_nano"),
Uri.parse("$expectedContent gif_tiny"),