mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 06:43:56 +01:00 
			
		
		
		
	Fix categories searching problem of photo uploading
This commit is contained in:
		
							parent
							
								
									63f1ed8a2d
								
							
						
					
					
						commit
						1e31497f10
					
				
					 1 changed files with 291 additions and 276 deletions
				
			
		|  | @ -1,5 +1,6 @@ | |||
| package fr.free.nrw.commons.upload.categories | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.text.TextUtils | ||||
| import androidx.lifecycle.LiveData | ||||
| import androidx.lifecycle.MutableLiveData | ||||
|  | @ -27,288 +28,302 @@ import javax.inject.Singleton | |||
|  */ | ||||
| @Singleton | ||||
| class CategoriesPresenter | ||||
| @Inject | ||||
| constructor( | ||||
|     private val repository: UploadRepository, | ||||
|     @param:Named(CommonsApplicationModule.IO_THREAD) private val ioScheduler: Scheduler, | ||||
|     @param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler, | ||||
| ) : CategoriesContract.UserActionListener { | ||||
|     companion object { | ||||
|         private val DUMMY: CategoriesContract.View = proxy() | ||||
|     } | ||||
| 
 | ||||
|     var view = DUMMY | ||||
|     private val compositeDisposable = CompositeDisposable() | ||||
|     private val searchTerms = PublishSubject.create<String>() | ||||
|     private var categoryList: MutableLiveData<List<CategoryItem>> = MutableLiveData() | ||||
| 
 | ||||
|     /** | ||||
|      * Current media | ||||
|      */ | ||||
|     private var media: Media? = null | ||||
| 
 | ||||
|     /** | ||||
|      * helper class for editing categories | ||||
|      */ | ||||
|     @Inject | ||||
|     constructor( | ||||
|         private val repository: UploadRepository, | ||||
|         @param:Named(CommonsApplicationModule.IO_THREAD) private val ioScheduler: Scheduler, | ||||
|         @param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler, | ||||
|     ) : CategoriesContract.UserActionListener { | ||||
|         companion object { | ||||
|             private val DUMMY: CategoriesContract.View = proxy() | ||||
|         } | ||||
|     lateinit var categoryEditHelper: CategoryEditHelper | ||||
| 
 | ||||
|         var view = DUMMY | ||||
|         private val compositeDisposable = CompositeDisposable() | ||||
|         private val searchTerms = PublishSubject.create<String>() | ||||
|         private var categoryList: MutableLiveData<List<CategoryItem>> = MutableLiveData() | ||||
| 
 | ||||
|         /** | ||||
|          * Current media | ||||
|          */ | ||||
|         private var media: Media? = null | ||||
| 
 | ||||
|         /** | ||||
|          * helper class for editing categories | ||||
|          */ | ||||
|         @Inject | ||||
|         lateinit var categoryEditHelper: CategoryEditHelper | ||||
| 
 | ||||
|         override fun onAttachView(view: CategoriesContract.View) { | ||||
|             this.view = view | ||||
|             compositeDisposable.add( | ||||
|                 searchTerms | ||||
|                     .observeOn(mainThreadScheduler) | ||||
|                     .doOnNext { | ||||
|                         view.showProgress(true) | ||||
|                     }.switchMap(::searchResults) | ||||
|                     .map { repository.selectedCategories + it } | ||||
|                     .map { it.distinctBy { categoryItem -> categoryItem.name } } | ||||
|                     .observeOn(mainThreadScheduler) | ||||
|                     .subscribe( | ||||
|                         { | ||||
|                             setCategoryListValue(it) | ||||
|                             view.showProgress(false) | ||||
|                             if (it.isEmpty()) { | ||||
|                                 view.showError(R.string.no_categories_found) | ||||
|                             } | ||||
|                         }, | ||||
|                         { t: Throwable? -> | ||||
|                             view.showProgress(false) | ||||
|     @SuppressLint("TimberArgCount") | ||||
|     override fun onAttachView(view: CategoriesContract.View) { | ||||
|         this.view = view | ||||
|         compositeDisposable.add( | ||||
|             searchTerms | ||||
|                 .observeOn(mainThreadScheduler) | ||||
|                 .doOnNext { | ||||
|                     view.showProgress(true) | ||||
|                 }.switchMap(::searchResults) | ||||
|                 .map { repository.selectedCategories + it } | ||||
|                 .map { it.distinctBy { categoryItem -> categoryItem.name } } | ||||
|                 .observeOn(mainThreadScheduler) | ||||
|                 .subscribe( | ||||
|                     { | ||||
|                         setCategoryListValue(it) | ||||
|                         view.showProgress(false) | ||||
|                         if (it.isEmpty() && !isInitialLoad) { | ||||
|                             view.showError(R.string.no_categories_found) | ||||
|                             Timber.e(t) | ||||
|                         }, | ||||
|                     ), | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * If media is null : Fetches categories from server according to the term | ||||
|          * Else : Fetches existing categories by their name, fetches categories from server according | ||||
|          * to the term and combines both in a list | ||||
|          */ | ||||
|         private fun searchResults(term: String): Observable<List<CategoryItem>>? { | ||||
|             if (media == null) { | ||||
|                 return repository | ||||
|                     .searchAll(term, getImageTitleList(), repository.selectedDepictions) | ||||
|                     .subscribeOn(ioScheduler) | ||||
|                     .map { | ||||
|                         it.filter { categoryItem -> | ||||
|                             !repository.isSpammyCategory(categoryItem.name) || | ||||
|                                 categoryItem.name == term | ||||
|                         } | ||||
|                     } | ||||
|             } else { | ||||
|                 return Observable | ||||
|                     .zip( | ||||
|                         repository | ||||
|                             .getCategories(repository.selectedExistingCategories) | ||||
|                             .map { list -> | ||||
|                                 list.map { | ||||
|                                     CategoryItem(it.name, it.description, it.thumbnail, true) | ||||
|                                 } | ||||
|                             }, | ||||
|                         repository.searchAll(term, getImageTitleList(), repository.selectedDepictions), | ||||
|                     ) { it1, it2 -> | ||||
|                         it1 + it2 | ||||
|                     }.subscribeOn(ioScheduler) | ||||
|                     .map { | ||||
|                         it.filter { categoryItem -> | ||||
|                             !repository.isSpammyCategory(categoryItem.name) || | ||||
|                     }, | ||||
|                     { t: Throwable? -> | ||||
|                         view.showProgress(false) | ||||
|                         view.showError(R.string.no_categories_found) | ||||
|                         Timber.e(t) | ||||
|                     }, | ||||
|                 ), | ||||
|         ) | ||||
| 
 | ||||
|         //isInitialLoad = false | ||||
|     } | ||||
| 
 | ||||
|     private var isInitialLoad = true //avoid initial empty content of edittext lead to showError | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * If media is null : Fetches categories from server according to the term | ||||
|      * Else : Fetches existing categories by their name, fetches categories from server according | ||||
|      * to the term and combines both in a list | ||||
|      */ | ||||
|     private fun searchResults(term: String): Observable<List<CategoryItem>>? { | ||||
|         if (media == null) { | ||||
|             return repository | ||||
|                 .searchAll(term, getImageTitleList(), repository.selectedDepictions) | ||||
|                 .subscribeOn(ioScheduler) | ||||
|                 .map { | ||||
|                     it.filter { categoryItem -> | ||||
|                         !repository.isSpammyCategory(categoryItem.name) || | ||||
|                                 categoryItem.name == term | ||||
|                         } | ||||
|                     }.map { it.filterNot { categoryItem -> categoryItem.thumbnail == "hidden" } } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         override fun onDetachView() { | ||||
|             view = DUMMY | ||||
|             compositeDisposable.clear() | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * asks the repository to fetch categories for the query | ||||
|          * @param query | ||||
|          */ | ||||
|         override fun searchForCategories(query: String) { | ||||
|             searchTerms.onNext(query) | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Returns image title list from UploadItem | ||||
|          * @return | ||||
|          */ | ||||
|         private fun getImageTitleList(): List<String> = | ||||
|             repository.uploads | ||||
|                 .map { it.uploadMediaDetails[0].captionText } | ||||
|                 .filterNot { TextUtils.isEmpty(it) } | ||||
| 
 | ||||
|         /** | ||||
|          * Verifies the number of categories selected, prompts the user if none selected | ||||
|          */ | ||||
|         override fun verifyCategories() { | ||||
|             val selectedCategories = repository.selectedCategories | ||||
|             if (selectedCategories.isNotEmpty()) { | ||||
|                 repository.setSelectedCategories(selectedCategories.map { it.name }) | ||||
|                 view.goToNextScreen() | ||||
|             } else { | ||||
|                 view.showNoCategorySelected() | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * ask repository to handle category clicked | ||||
|          * | ||||
|          * @param categoryItem | ||||
|          */ | ||||
|         override fun onCategoryItemClicked(categoryItem: CategoryItem) { | ||||
|             repository.onCategoryClicked(categoryItem, media) | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Attaches view and media | ||||
|          */ | ||||
|         override fun onAttachViewWithMedia( | ||||
|             view: CategoriesContract.View, | ||||
|             media: Media, | ||||
|         ) { | ||||
|             this.view = view | ||||
|             this.media = media | ||||
|             repository.selectedExistingCategories = view.existingCategories | ||||
|             compositeDisposable.add( | ||||
|                 searchTerms | ||||
|                     .observeOn(mainThreadScheduler) | ||||
|                     .doOnNext { | ||||
|                         view.showProgress(true) | ||||
|                     }.switchMap(::searchResults) | ||||
|                     .map { repository.selectedCategories + it } | ||||
|                     .map { it.distinctBy { categoryItem -> categoryItem.name } } | ||||
|                     .observeOn(mainThreadScheduler) | ||||
|                     .subscribe( | ||||
|                         { | ||||
|                             setCategoryListValue(it) | ||||
|                             view.showProgress(false) | ||||
|                             if (it.isEmpty()) { | ||||
|                                 view.showError(R.string.no_categories_found) | ||||
|                             } | ||||
|                         }, | ||||
|                         { t: Throwable? -> | ||||
|                             view.showProgress(false) | ||||
|                             view.showError(R.string.no_categories_found) | ||||
|                             Timber.e(t) | ||||
|                         }, | ||||
|                     ), | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Clears previous selections | ||||
|          */ | ||||
|         override fun clearPreviousSelection() { | ||||
|             repository.cleanup() | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Gets the selected categories and send them for posting to the server | ||||
|          * | ||||
|          * @param media media | ||||
|          * @param wikiText current WikiText from server | ||||
|          */ | ||||
|         override fun updateCategories( | ||||
|             media: Media, | ||||
|             wikiText: String, | ||||
|         ) { | ||||
|             // check if view.existingCategories is null | ||||
|             if (repository.selectedCategories.isNotEmpty() || | ||||
|                 (view.existingCategories != null && repository.selectedExistingCategories.size != view.existingCategories.size) | ||||
|             ) { | ||||
|                 val selectedCategories: MutableList<String> = | ||||
|                     ( | ||||
|                         repository.selectedCategories.map { it.name }.toMutableList() + | ||||
|                             repository.selectedExistingCategories | ||||
|                     ).toMutableList() | ||||
| 
 | ||||
|                 if (selectedCategories.isNotEmpty()) { | ||||
|                     view.showProgressDialog() | ||||
| 
 | ||||
|                     try { | ||||
|                         compositeDisposable.add( | ||||
|                             categoryEditHelper | ||||
|                                 .makeCategoryEdit( | ||||
|                                     view.fragmentContext, | ||||
|                                     media, | ||||
|                                     selectedCategories, | ||||
|                                     wikiText, | ||||
|                                 ).subscribeOn(Schedulers.io()) | ||||
|                                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                                 .subscribe({ | ||||
|                                     Timber.d("Categories are added.") | ||||
|                                     media.addedCategories = selectedCategories | ||||
|                                     repository.cleanup() | ||||
|                                     view.dismissProgressDialog() | ||||
|                                     view.refreshCategories() | ||||
|                                     view.goBackToPreviousScreen() | ||||
|                                 }, { | ||||
|                                     Timber.e( | ||||
|                                         "Failed to update categories", | ||||
|                                     ) | ||||
|                                 }), | ||||
|                         ) | ||||
|                     } catch (e: InvalidLoginTokenException) { | ||||
|                         view.navigateToLoginScreen() | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 repository.cleanup() | ||||
|                 view.showNoCategorySelected() | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Selects each [CategoryItem] in a given list as if they were clicked by the user by calling | ||||
|          * [onCategoryItemClicked] for each category and adding the category to [categoryList] | ||||
|          */ | ||||
|         private fun selectNewCategories(toSelect: List<CategoryItem>) { | ||||
|             toSelect.forEach { | ||||
|                 it.isSelected = true | ||||
|                 repository.onCategoryClicked(it, media) | ||||
|             } | ||||
| 
 | ||||
|             // Add the new selections to the list of category items so that the selections appear | ||||
|             // immediately (i.e. without any search term queries) | ||||
|             categoryList.value | ||||
|                 ?.toMutableList() | ||||
|                 ?.let { toSelect + it } | ||||
|                 ?.distinctBy(CategoryItem::name) | ||||
|                 ?.let { setCategoryListValue(it) } | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Livedata being used to observe category list inside | ||||
|          * @see UploadCategoriesFragment | ||||
|          * Any changes to category list reflect immediately to the adapter list | ||||
|          */ | ||||
|         override fun getCategories(): LiveData<List<CategoryItem>> = categoryList | ||||
| 
 | ||||
|         /** | ||||
|          * needed for tests | ||||
|          */ | ||||
|         fun setCategoryList(categoryList: MutableLiveData<List<CategoryItem>>) { | ||||
|             this.categoryList = categoryList | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * needed for tests | ||||
|          */ | ||||
|         fun setCategoryListValue(categoryItems: List<CategoryItem>) { | ||||
|             categoryList.postValue(categoryItems) | ||||
|         } | ||||
| 
 | ||||
|         override fun selectCategories() { | ||||
|             compositeDisposable.add( | ||||
|                 repository.placeCategories | ||||
|                     .subscribeOn(ioScheduler) | ||||
|                     .observeOn(mainThreadScheduler) | ||||
|                     .subscribe(::selectNewCategories), | ||||
|             ) | ||||
|         } else { | ||||
|             return Observable | ||||
|                 .zip( | ||||
|                     repository | ||||
|                         .getCategories(repository.selectedExistingCategories) | ||||
|                         .map { list -> | ||||
|                             list.map { | ||||
|                                 CategoryItem(it.name, it.description, it.thumbnail, true) | ||||
|                             } | ||||
|                         }, | ||||
|                     repository.searchAll(term, getImageTitleList(), repository.selectedDepictions), | ||||
|                 ) { it1, it2 -> | ||||
|                     it1 + it2 | ||||
|                 }.subscribeOn(ioScheduler) | ||||
|                 .map { | ||||
|                     it.filter { categoryItem -> | ||||
|                         !repository.isSpammyCategory(categoryItem.name) || | ||||
|                                 categoryItem.name == term | ||||
|                     } | ||||
|                 }.map { it.filterNot { categoryItem -> categoryItem.thumbnail == "hidden" } } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onDetachView() { | ||||
|         view = DUMMY | ||||
|         compositeDisposable.clear() | ||||
|         isInitialLoad = true | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * asks the repository to fetch categories for the query | ||||
|      * @param query | ||||
|      */ | ||||
|     override fun searchForCategories(query: String) { | ||||
|         if (query.isBlank()) { | ||||
|             if (!isInitialLoad) { | ||||
|                 view.showError(R.string.no_categories_found) | ||||
|             } | ||||
|             return | ||||
|         } | ||||
|         isInitialLoad = false | ||||
|         searchTerms.onNext(query) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns image title list from UploadItem | ||||
|      * @return | ||||
|      */ | ||||
|     private fun getImageTitleList(): List<String> = | ||||
|         repository.uploads | ||||
|             .map { it.uploadMediaDetails[0].captionText } | ||||
|             .filterNot { TextUtils.isEmpty(it) } | ||||
| 
 | ||||
|     /** | ||||
|      * Verifies the number of categories selected, prompts the user if none selected | ||||
|      */ | ||||
|     override fun verifyCategories() { | ||||
|         val selectedCategories = repository.selectedCategories | ||||
|         if (selectedCategories.isNotEmpty()) { | ||||
|             repository.setSelectedCategories(selectedCategories.map { it.name }) | ||||
|             view.goToNextScreen() | ||||
|         } else { | ||||
|             view.showNoCategorySelected() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * ask repository to handle category clicked | ||||
|      * | ||||
|      * @param categoryItem | ||||
|      */ | ||||
|     override fun onCategoryItemClicked(categoryItem: CategoryItem) { | ||||
|         repository.onCategoryClicked(categoryItem, media) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Attaches view and media | ||||
|      */ | ||||
|     override fun onAttachViewWithMedia( | ||||
|         view: CategoriesContract.View, | ||||
|         media: Media, | ||||
|     ) { | ||||
|         this.view = view | ||||
|         this.media = media | ||||
|         repository.selectedExistingCategories = view.existingCategories | ||||
|         compositeDisposable.add( | ||||
|             searchTerms | ||||
|                 .observeOn(mainThreadScheduler) | ||||
|                 .doOnNext { | ||||
|                     view.showProgress(true) | ||||
|                 }.switchMap(::searchResults) | ||||
|                 .map { repository.selectedCategories + it } | ||||
|                 .map { it.distinctBy { categoryItem -> categoryItem.name } } | ||||
|                 .observeOn(mainThreadScheduler) | ||||
|                 .subscribe( | ||||
|                     { | ||||
|                         setCategoryListValue(it) | ||||
|                         view.showProgress(false) | ||||
|                         if (it.isEmpty() && !isInitialLoad) { | ||||
|                             view.showError(R.string.no_categories_found) | ||||
|                         } | ||||
|                     }, | ||||
|                     { t: Throwable? -> | ||||
|                         view.showProgress(false) | ||||
|                         view.showError(R.string.no_categories_found) | ||||
|                         Timber.e(t) | ||||
|                     }, | ||||
|                 ), | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Clears previous selections | ||||
|      */ | ||||
|     override fun clearPreviousSelection() { | ||||
|         repository.cleanup() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the selected categories and send them for posting to the server | ||||
|      * | ||||
|      * @param media media | ||||
|      * @param wikiText current WikiText from server | ||||
|      */ | ||||
|     override fun updateCategories( | ||||
|         media: Media, | ||||
|         wikiText: String, | ||||
|     ) { | ||||
|         // check if view.existingCategories is null | ||||
|         if (repository.selectedCategories.isNotEmpty() || | ||||
|             (view.existingCategories != null && repository.selectedExistingCategories.size != view.existingCategories.size) | ||||
|         ) { | ||||
|             val selectedCategories: MutableList<String> = | ||||
|                 ( | ||||
|                         repository.selectedCategories.map { it.name }.toMutableList() + | ||||
|                                 repository.selectedExistingCategories | ||||
|                         ).toMutableList() | ||||
| 
 | ||||
|             if (selectedCategories.isNotEmpty()) { | ||||
|                 view.showProgressDialog() | ||||
| 
 | ||||
|                 try { | ||||
|                     compositeDisposable.add( | ||||
|                         categoryEditHelper | ||||
|                             .makeCategoryEdit( | ||||
|                                 view.fragmentContext, | ||||
|                                 media, | ||||
|                                 selectedCategories, | ||||
|                                 wikiText, | ||||
|                             ).subscribeOn(Schedulers.io()) | ||||
|                             .observeOn(AndroidSchedulers.mainThread()) | ||||
|                             .subscribe({ | ||||
|                                 Timber.d("Categories are added.") | ||||
|                                 media.addedCategories = selectedCategories | ||||
|                                 repository.cleanup() | ||||
|                                 view.dismissProgressDialog() | ||||
|                                 view.refreshCategories() | ||||
|                                 view.goBackToPreviousScreen() | ||||
|                             }, { | ||||
|                                 Timber.e( | ||||
|                                     "Failed to update categories", | ||||
|                                 ) | ||||
|                             }), | ||||
|                     ) | ||||
|                 } catch (e: InvalidLoginTokenException) { | ||||
|                     view.navigateToLoginScreen() | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             repository.cleanup() | ||||
|             view.showNoCategorySelected() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Selects each [CategoryItem] in a given list as if they were clicked by the user by calling | ||||
|      * [onCategoryItemClicked] for each category and adding the category to [categoryList] | ||||
|      */ | ||||
|     private fun selectNewCategories(toSelect: List<CategoryItem>) { | ||||
|         toSelect.forEach { | ||||
|             it.isSelected = true | ||||
|             repository.onCategoryClicked(it, media) | ||||
|         } | ||||
| 
 | ||||
|         // Add the new selections to the list of category items so that the selections appear | ||||
|         // immediately (i.e. without any search term queries) | ||||
|         categoryList.value | ||||
|             ?.toMutableList() | ||||
|             ?.let { toSelect + it } | ||||
|             ?.distinctBy(CategoryItem::name) | ||||
|             ?.let { setCategoryListValue(it) } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Livedata being used to observe category list inside | ||||
|      * @see UploadCategoriesFragment | ||||
|      * Any changes to category list reflect immediately to the adapter list | ||||
|      */ | ||||
|     override fun getCategories(): LiveData<List<CategoryItem>> = categoryList | ||||
| 
 | ||||
|     /** | ||||
|      * needed for tests | ||||
|      */ | ||||
|     fun setCategoryList(categoryList: MutableLiveData<List<CategoryItem>>) { | ||||
|         this.categoryList = categoryList | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * needed for tests | ||||
|      */ | ||||
|     fun setCategoryListValue(categoryItems: List<CategoryItem>) { | ||||
|         categoryList.postValue(categoryItems) | ||||
|     } | ||||
| 
 | ||||
|     override fun selectCategories() { | ||||
|         compositeDisposable.add( | ||||
|             repository.placeCategories | ||||
|                 .subscribeOn(ioScheduler) | ||||
|                 .observeOn(mainThreadScheduler) | ||||
|                 .subscribe(::selectNewCategories), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Zihan Pan
						Zihan Pan