diff --git a/godtools.xcodeproj/project.pbxproj b/godtools.xcodeproj/project.pbxproj index 8ef78e033..2cb59ada9 100644 --- a/godtools.xcodeproj/project.pbxproj +++ b/godtools.xcodeproj/project.pbxproj @@ -1537,6 +1537,7 @@ D43FC1D22B814B8B00F8310E /* AccountInterfaceStringsDomainModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43FC1D12B814B8B00F8310E /* AccountInterfaceStringsDomainModel.swift */; }; D43FC1D42B814BDE00F8310E /* GetAccountInterfaceStringsRepositoryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43FC1D32B814BDE00F8310E /* GetAccountInterfaceStringsRepositoryInterface.swift */; }; D43FC1D62B814F2000F8310E /* GetAccountInterfaceStringsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43FC1D52B814F2000F8310E /* GetAccountInterfaceStringsRepository.swift */; }; + D44CE4F82DA1AF6C0009F4D1 /* FavoritedResourcesRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44CE4F72DA1AF610009F4D1 /* FavoritedResourcesRepositoryTests.swift */; }; D455F3ED2977302D009D5F93 /* GetUserActivityUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D455F3EC2977302D009D5F93 /* GetUserActivityUseCase.swift */; }; D455F3F0297739F6009D5F93 /* AccountActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D455F3EF297739F6009D5F93 /* AccountActivityView.swift */; }; D455F3F229773C1B009D5F93 /* UserActivityDomainModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D455F3F129773C1B009D5F93 /* UserActivityDomainModel.swift */; }; @@ -1566,6 +1567,9 @@ D4960CDC2CE694F00090B114 /* GetResumeLessonProgressModalInterfaceStringsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4960CDB2CE694F00090B114 /* GetResumeLessonProgressModalInterfaceStringsUseCase.swift */; }; D496B9062A9E702200CBEA19 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D496B9052A9E702200CBEA19 /* SearchBar.swift */; }; D4997E0E28D4CED800205B4C /* YouTubeiOSPlayerHelper in Frameworks */ = {isa = PBXBuildFile; productRef = D4997E0D28D4CED800205B4C /* YouTubeiOSPlayerHelper */; }; + D4AAEC152D972CF8008CDF8E /* ReorderFavoritedToolDomainModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AAEC142D972CF8008CDF8E /* ReorderFavoritedToolDomainModel.swift */; }; + D4AAEC192D97307F008CDF8E /* RemoveFavoritedToolRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AAEC182D973074008CDF8E /* RemoveFavoritedToolRepositoryTests.swift */; }; + D4AAEC1B2D9C68D2008CDF8E /* ToggleToolFavoritedRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AAEC1A2D9C68BF008CDF8E /* ToggleToolFavoritedRepositoryTests.swift */; }; D4AFA4452BAC93B400318023 /* ToolFilterLanguagesInterfaceStringsDomainModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AFA4442BAC93B400318023 /* ToolFilterLanguagesInterfaceStringsDomainModel.swift */; }; D4AFA4472BAC950800318023 /* GetToolFilterLanguagesInterfaceStringsRepositoryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AFA4462BAC950800318023 /* GetToolFilterLanguagesInterfaceStringsRepositoryInterface.swift */; }; D4AFA4492BAC955E00318023 /* GetToolFilterLanguagesInterfaceStringsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AFA4482BAC955E00318023 /* GetToolFilterLanguagesInterfaceStringsRepository.swift */; }; @@ -1578,6 +1582,10 @@ D4B060022912C335005852D0 /* MobileContentAuthTokenCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B060012912C335005852D0 /* MobileContentAuthTokenCache.swift */; }; D4B060042913096A005852D0 /* MobileContentAuthTokenKeychainAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B060032913096A005852D0 /* MobileContentAuthTokenKeychainAccessor.swift */; }; D4B060072915647B005852D0 /* KeychainServiceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B060062915647B005852D0 /* KeychainServiceResponse.swift */; }; + D4B26F502D8B440F0019FA86 /* ReorderFavoritedToolUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B26F4F2D8B440F0019FA86 /* ReorderFavoritedToolUseCase.swift */; }; + D4B26F522D8B44AF0019FA86 /* ReorderFavoritedToolRepositoryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B26F512D8B44AF0019FA86 /* ReorderFavoritedToolRepositoryInterface.swift */; }; + D4B26F542D8B45330019FA86 /* ReorderFavoritedToolRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B26F532D8B45330019FA86 /* ReorderFavoritedToolRepository.swift */; }; + D4B26F592D8DF0A80019FA86 /* ReorderFavoritedToolRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B26F582D8DF0A80019FA86 /* ReorderFavoritedToolRepositoryTests.swift */; }; D4B522D929D761B700D85213 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D4B522DB29D761B700D85213 /* Localizable.stringsdict */; }; D4BC79D5299D6BFB0040651B /* CompletedTrainingTipRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4BC79D4299D6BFB0040651B /* CompletedTrainingTipRepository.swift */; }; D4BC79DA299D6C7D0040651B /* RealmCompletedTrainingTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4BC79D9299D6C7D0040651B /* RealmCompletedTrainingTip.swift */; }; @@ -3270,6 +3278,7 @@ D43FC1D12B814B8B00F8310E /* AccountInterfaceStringsDomainModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInterfaceStringsDomainModel.swift; sourceTree = ""; }; D43FC1D32B814BDE00F8310E /* GetAccountInterfaceStringsRepositoryInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAccountInterfaceStringsRepositoryInterface.swift; sourceTree = ""; }; D43FC1D52B814F2000F8310E /* GetAccountInterfaceStringsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAccountInterfaceStringsRepository.swift; sourceTree = ""; }; + D44CE4F72DA1AF610009F4D1 /* FavoritedResourcesRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritedResourcesRepositoryTests.swift; sourceTree = ""; }; D455F3EC2977302D009D5F93 /* GetUserActivityUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetUserActivityUseCase.swift; sourceTree = ""; }; D455F3EF297739F6009D5F93 /* AccountActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityView.swift; sourceTree = ""; }; D455F3F129773C1B009D5F93 /* UserActivityDomainModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityDomainModel.swift; sourceTree = ""; }; @@ -3298,6 +3307,9 @@ D4960CD92CE694280090B114 /* GetResumeLessonProgressModalInterfaceStringsRepositoryInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetResumeLessonProgressModalInterfaceStringsRepositoryInterface.swift; sourceTree = ""; }; D4960CDB2CE694F00090B114 /* GetResumeLessonProgressModalInterfaceStringsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetResumeLessonProgressModalInterfaceStringsUseCase.swift; sourceTree = ""; }; D496B9052A9E702200CBEA19 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; + D4AAEC142D972CF8008CDF8E /* ReorderFavoritedToolDomainModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderFavoritedToolDomainModel.swift; sourceTree = ""; }; + D4AAEC182D973074008CDF8E /* RemoveFavoritedToolRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveFavoritedToolRepositoryTests.swift; sourceTree = ""; }; + D4AAEC1A2D9C68BF008CDF8E /* ToggleToolFavoritedRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleToolFavoritedRepositoryTests.swift; sourceTree = ""; }; D4AFA4442BAC93B400318023 /* ToolFilterLanguagesInterfaceStringsDomainModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolFilterLanguagesInterfaceStringsDomainModel.swift; sourceTree = ""; }; D4AFA4462BAC950800318023 /* GetToolFilterLanguagesInterfaceStringsRepositoryInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetToolFilterLanguagesInterfaceStringsRepositoryInterface.swift; sourceTree = ""; }; D4AFA4482BAC955E00318023 /* GetToolFilterLanguagesInterfaceStringsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetToolFilterLanguagesInterfaceStringsRepository.swift; sourceTree = ""; }; @@ -3310,6 +3322,10 @@ D4B060012912C335005852D0 /* MobileContentAuthTokenCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileContentAuthTokenCache.swift; sourceTree = ""; }; D4B060032913096A005852D0 /* MobileContentAuthTokenKeychainAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileContentAuthTokenKeychainAccessor.swift; sourceTree = ""; }; D4B060062915647B005852D0 /* KeychainServiceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainServiceResponse.swift; sourceTree = ""; }; + D4B26F4F2D8B440F0019FA86 /* ReorderFavoritedToolUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderFavoritedToolUseCase.swift; sourceTree = ""; }; + D4B26F512D8B44AF0019FA86 /* ReorderFavoritedToolRepositoryInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderFavoritedToolRepositoryInterface.swift; sourceTree = ""; }; + D4B26F532D8B45330019FA86 /* ReorderFavoritedToolRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderFavoritedToolRepository.swift; sourceTree = ""; }; + D4B26F582D8DF0A80019FA86 /* ReorderFavoritedToolRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderFavoritedToolRepositoryTests.swift; sourceTree = ""; }; D4B522DC29D761BA00D85213 /* af */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = af; path = af.lproj/Localizable.stringsdict; sourceTree = ""; }; D4B522DD29D761BA00D85213 /* sq */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sq; path = sq.lproj/Localizable.stringsdict; sourceTree = ""; }; D4B522DE29D761BB00D85213 /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = am; path = am.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -4911,6 +4927,7 @@ 45B2E7182BC044F70077A3C3 /* AppLanguage */, 453E2A0F2BC8C86A0047D4C6 /* Dashboard */, 45BA7B832AF2E0AC001BAB31 /* DownloadToolProgress */, + D4B26F552D8DF0320019FA86 /* Favorites */, 45308EC22BDC313B00A49D96 /* LessonEvaluation */, 4505F5A02C41549900F60F94 /* LessonFilter */, 45243C552C58221A00BD6F49 /* Lessons */, @@ -7274,6 +7291,7 @@ 452F27972B8399FD00844B9A /* GetToolIsFavoritedRepositoryInterface.swift */, 45D5EA8D2B7E4C2D00745F0D /* GetYourFavoritedToolsRepositoryInterface.swift */, 452F27892B838CC300844B9A /* RemoveFavoritedToolRepositoryInterface.swift */, + D4B26F512D8B44AF0019FA86 /* ReorderFavoritedToolRepositoryInterface.swift */, 452F278F2B83931500844B9A /* ToggleToolFavoritedRepositoryInterface.swift */, ); path = Interface; @@ -7288,6 +7306,7 @@ 452DFB382B7FB3E3003A4A59 /* ViewAllYourFavoritedToolsUseCase.swift */, 45D5EA972B7E5B3600745F0D /* ViewConfirmRemoveToolFromFavoritesUseCase.swift */, 458B91BC2B7D66F000785C6F /* ViewFavoritesUseCase.swift */, + D4B26F4F2D8B440F0019FA86 /* ReorderFavoritedToolUseCase.swift */, ); path = UseCases; sourceTree = ""; @@ -7298,6 +7317,7 @@ 452DFB402B7FB4B4003A4A59 /* AllYourFavoritedToolsInterfaceStringsDomainModel.swift */, 45D5EA9B2B7E5B9300745F0D /* ConfirmRemoveToolFromFavoritesInterfaceStringsDomainModel.swift */, 458B91C02B7D672100785C6F /* FavoritesInterfaceStringsDomainModel.swift */, + D4AAEC142D972CF8008CDF8E /* ReorderFavoritedToolDomainModel.swift */, 452F27932B83940F00844B9A /* ToolIsFavoritedDomainModel.swift */, 452DFB3A2B7FB40F003A4A59 /* ViewAllYourFavoritedToolsDomainModel.swift */, 45D5EA992B7E5B7900745F0D /* ViewConfirmRemoveToolFromFavoritesDomainModel.swift */, @@ -7317,6 +7337,7 @@ 45D5EA8F2B7E4C5400745F0D /* GetYourFavoritedToolsRepository.swift */, 452F278B2B838CEC00844B9A /* RemoveFavoritedToolRepository.swift */, 452F27912B83933C00844B9A /* ToggleToolFavoritedRepository.swift */, + D4B26F532D8B45330019FA86 /* ReorderFavoritedToolRepository.swift */, ); path = "Data-DomainInterface"; sourceTree = ""; @@ -11722,6 +11743,25 @@ path = Cache; sourceTree = ""; }; + D4B26F552D8DF0320019FA86 /* Favorites */ = { + isa = PBXGroup; + children = ( + D4B26F5A2D8DF45F0019FA86 /* Data-DomainInterface */, + ); + path = Favorites; + sourceTree = ""; + }; + D4B26F5A2D8DF45F0019FA86 /* Data-DomainInterface */ = { + isa = PBXGroup; + children = ( + D44CE4F72DA1AF610009F4D1 /* FavoritedResourcesRepositoryTests.swift */, + D4AAEC182D973074008CDF8E /* RemoveFavoritedToolRepositoryTests.swift */, + D4B26F582D8DF0A80019FA86 /* ReorderFavoritedToolRepositoryTests.swift */, + D4AAEC1A2D9C68BF008CDF8E /* ToggleToolFavoritedRepositoryTests.swift */, + ); + path = "Data-DomainInterface"; + sourceTree = ""; + }; D4BC79D6299D6C050040651B /* CompletedTrainingTipRepository */ = { isa = PBXGroup; children = ( @@ -12723,6 +12763,7 @@ 45D08D702AFABF4400ADA673 /* ToolSettingsFlowCompletedState.swift in Sources */, 4598BD2C2BF7DF6800196463 /* SocialSignInView.swift in Sources */, D4C2A5902C3C6F2D0035AB4D /* GetUserLessonFiltersRepositoryInterface.swift in Sources */, + D4B26F502D8B440F0019FA86 /* ReorderFavoritedToolUseCase.swift in Sources */, 453F84D02A029FC00005101E /* ArticleAemDownloaderError.swift in Sources */, 452DFB3F2B7FB464003A4A59 /* GetAllYourFavoritedToolsInterfaceStringsRepository.swift in Sources */, 45369ACF2AFA7FA500BD10F0 /* DidViewToolScreenShareTutorialUseCase.swift in Sources */, @@ -13059,6 +13100,7 @@ 45E60D022AEAE1E600E14BEA /* CircledTextView.swift in Sources */, 450D7B0328E32965006C3FDF /* TrackDownloadedTranslationsRepository.swift in Sources */, 45FB161327DBDDB60009DF8E /* ChooseYourOwnAdventureFlowCompletedState.swift in Sources */, + D4B26F522D8B44AF0019FA86 /* ReorderFavoritedToolRepositoryInterface.swift in Sources */, 45AD20BC25938F1A00A096A0 /* SignalValue.swift in Sources */, 455E98B029A42ABB00D1EF35 /* OnboardingTutorialMediaViewModel.swift in Sources */, 45F7B0B82AF18E9000A0E7B4 /* GetMenuInterfaceStringsRepositoryInterface.swift in Sources */, @@ -13383,6 +13425,7 @@ 456AC19E2B8CEE6200169C11 /* ToolSettingsLanguages.swift in Sources */, 45D63E73288F698C009B4610 /* LanguageModel.swift in Sources */, 45B3F4662AC3B1B800D61BFD /* GetAppInterfaceLayoutDirectionInterface.swift in Sources */, + D4AAEC152D972CF8008CDF8E /* ReorderFavoritedToolDomainModel.swift in Sources */, 45C2AF3B2A813371004958AB /* PageControlButton.swift in Sources */, D43FC1C82B69976200F8310E /* GetDownloadedLanguagesListRepositoryInterface.swift in Sources */, 4598BD102BF7DF6800196463 /* AuthenticationProviderProfile.swift in Sources */, @@ -14097,6 +14140,7 @@ 455CB57E2A439B6700E95834 /* LearnToShareToolView.swift in Sources */, 45FB161127DBDDB60009DF8E /* ToolNavigationFlow.swift in Sources */, 452486B92B07C894007AD932 /* ToolFilterLanguageSelectionView.swift in Sources */, + D4B26F542D8B45330019FA86 /* ReorderFavoritedToolRepository.swift in Sources */, 45AE975627C97A9500C2CB33 /* Spacer+MobileContentRenderableModel.swift in Sources */, 45A8E4392AFC0B1A008EF03D /* GetCreatingToolScreenShareSessionInterfaceStringsRepository.swift in Sources */, 453F84D52A029FC00005101E /* ArticleAemRepositoryResult.swift in Sources */, @@ -14139,6 +14183,7 @@ files = ( 459ABF612B0BB29D0034499D /* RealmResourcesCacheTests.swift in Sources */, 45E6BBEC2C41642800A49960 /* GetTranslatedLanguageNameTests.swift in Sources */, + D4AAEC1B2D9C68D2008CDF8E /* ToggleToolFavoritedRepositoryTests.swift in Sources */, 45D7783029A572CC00F23D99 /* PassthroughValue.swift in Sources */, 45E198692BA4822A00BF14F3 /* TestsDiContainer.swift in Sources */, 450FB88A2BFBA0F80015D945 /* StoreInitialAppLanguageTests.swift in Sources */, @@ -14164,10 +14209,13 @@ 45243C5B2C5828C600BD6F49 /* MockRealmTranslation.swift in Sources */, 45CBDA092BA3930B0007DEC8 /* MockLaunchCountRepository.swift in Sources */, 45EB68E12C334D84008A5FF2 /* MockLocalizationServices.swift in Sources */, + D44CE4F82DA1AF6C0009F4D1 /* FavoritedResourcesRepositoryTests.swift in Sources */, + D4AAEC192D97307F008CDF8E /* RemoveFavoritedToolRepositoryTests.swift in Sources */, 45B9B07B2CDEB2D6001DF554 /* MobileContentBackgroundImageRendererTests.swift in Sources */, 45368A182ABB2D850028A570 /* TestRealmObject.swift in Sources */, 453E2A122BC8C8810047D4C6 /* GetToolsRepositoryTests.swift in Sources */, 45F357B62C52E49200F31EB2 /* GetLessonFilterLanguagesRepositoryTests.swift in Sources */, + D4B26F592D8DF0A80019FA86 /* ReorderFavoritedToolRepositoryTests.swift in Sources */, 45368A192ABB2D850028A570 /* DeepLinkingServiceTests.swift in Sources */, 45192F522C470AEC005740E5 /* UserCountersAPIMock.swift in Sources */, 45CC43012C38982800B0D82B /* MockLocaleLanguageRegionName.swift in Sources */, diff --git a/godtools/App/Features/Dashboard/Data-DomainInterface/FavoritedToolsLatestToolDownloader.swift b/godtools/App/Features/Dashboard/Data-DomainInterface/FavoritedToolsLatestToolDownloader.swift index c646cd09c..c4ff5f8aa 100644 --- a/godtools/App/Features/Dashboard/Data-DomainInterface/FavoritedToolsLatestToolDownloader.swift +++ b/godtools/App/Features/Dashboard/Data-DomainInterface/FavoritedToolsLatestToolDownloader.swift @@ -30,7 +30,7 @@ class FavoritedToolsLatestToolDownloader: FavoritedToolsLatestToolDownloaderInte ) .flatMap({ (resourcesChanged: Void, favoritedResourcesChanged: Void) -> AnyPublisher<[FavoritedResourceDataModel], Never> in - let favoritedTools: [FavoritedResourceDataModel] = self.favoritedResourcesRepository.getFavoritedResourcesSortedByCreatedAt(ascendingOrder: false) + let favoritedTools: [FavoritedResourceDataModel] = self.favoritedResourcesRepository.getFavoritedResourcesSortedByPosition() return Just(favoritedTools) .eraseToAnyPublisher() diff --git a/godtools/App/Features/Dashboard/Data-DomainInterface/StoreInitialFavoritedTools.swift b/godtools/App/Features/Dashboard/Data-DomainInterface/StoreInitialFavoritedTools.swift index de226bf17..9f85ea4df 100644 --- a/godtools/App/Features/Dashboard/Data-DomainInterface/StoreInitialFavoritedTools.swift +++ b/godtools/App/Features/Dashboard/Data-DomainInterface/StoreInitialFavoritedTools.swift @@ -29,13 +29,5 @@ class StoreInitialFavoritedTools: StoreInitialFavoritedToolsInterface { return favoritedResourcesRepository .storeFavoritedResourcesPublisher(ids: favoritedResourceIdsToStore) - .catch { _ in - return Just([]) - .eraseToAnyPublisher() - } - .map { _ in - Void() - } - .eraseToAnyPublisher() } } diff --git a/godtools/App/Features/Favorites/Data-DomainInterface/GetYourFavoritedToolsRepository.swift b/godtools/App/Features/Favorites/Data-DomainInterface/GetYourFavoritedToolsRepository.swift index d58f17803..cddffdc6e 100644 --- a/godtools/App/Features/Favorites/Data-DomainInterface/GetYourFavoritedToolsRepository.swift +++ b/godtools/App/Features/Favorites/Data-DomainInterface/GetYourFavoritedToolsRepository.swift @@ -29,16 +29,15 @@ class GetYourFavoritedToolsRepository: GetYourFavoritedToolsRepositoryInterface func getToolsPublisher(translateInLanguage: AppLanguageDomainModel, maxCount: Int?) -> AnyPublisher<[YourFavoritedToolDomainModel], Never> { return Publishers.CombineLatest3( - favoritedResourcesRepository.getFavoritedResourcesChangedPublisher(), resourcesRepository.getResourcesChangedPublisher(), - getToolListItemInterfaceStringsRepository.getStringsPublisher(translateInLanguage: translateInLanguage) + getToolListItemInterfaceStringsRepository.getStringsPublisher(translateInLanguage: translateInLanguage), + favoritedResourcesRepository.getFavoritedResourcesSortedByPositionPublisher() ) - .flatMap({ (favoritedResourcesChanged: Void, resourcesChanged: Void, interfaceStrings: ToolListItemInterfaceStringsDomainModel) -> AnyPublisher<[YourFavoritedToolDomainModel], Never> in + .flatMap({ (resourcesChanged: Void, interfaceStrings: ToolListItemInterfaceStringsDomainModel, favoritedResourceModels: [FavoritedResourceDataModel]) -> AnyPublisher<[YourFavoritedToolDomainModel], Never> in let numberOfFavoritedTools: Int = self.favoritedResourcesRepository.getNumberOfFavoritedResources() - let favoritedResources: [ResourceModel] = self.favoritedResourcesRepository - .getFavoritedResourcesSortedByCreatedAt(ascendingOrder: false) + let favoritedResources: [ResourceModel] = favoritedResourceModels .prefix(maxCount ?? numberOfFavoritedTools) .compactMap({ self.resourcesRepository.getResource(id: $0.id) diff --git a/godtools/App/Features/Favorites/Data-DomainInterface/ReorderFavoritedToolRepository.swift b/godtools/App/Features/Favorites/Data-DomainInterface/ReorderFavoritedToolRepository.swift new file mode 100644 index 000000000..0f86c2b69 --- /dev/null +++ b/godtools/App/Features/Favorites/Data-DomainInterface/ReorderFavoritedToolRepository.swift @@ -0,0 +1,30 @@ +// +// ReorderFavoritedToolRepository.swift +// godtools +// +// Created by Rachael Skeath on 3/19/25. +// Copyright © 2025 Cru. All rights reserved. +// + +import Foundation +import Combine + +class ReorderFavoritedToolRepository: ReorderFavoritedToolRepositoryInterface { + + private let favoritedResourcesRepository: FavoritedResourcesRepository + + init(favoritedResourcesRepository: FavoritedResourcesRepository) { + self.favoritedResourcesRepository = favoritedResourcesRepository + } + + func reorderFavoritedToolPubilsher(toolId: String, originalPosition: Int, newPosition: Int) -> AnyPublisher<[ReorderFavoritedToolDomainModel], Error> { + + return favoritedResourcesRepository.reorderFavoritedResourcePublisher(id: toolId, originalPosition: originalPosition, newPosition: newPosition) + .map { favoritesReordered in + return favoritesReordered.map { + ReorderFavoritedToolDomainModel(dataModelId: $0.id, position: $0.position) + } + } + .eraseToAnyPublisher() + } +} diff --git a/godtools/App/Features/Favorites/Data-DomainInterface/ToggleToolFavoritedRepository.swift b/godtools/App/Features/Favorites/Data-DomainInterface/ToggleToolFavoritedRepository.swift index 4f803e385..2dc50c0bf 100644 --- a/godtools/App/Features/Favorites/Data-DomainInterface/ToggleToolFavoritedRepository.swift +++ b/godtools/App/Features/Favorites/Data-DomainInterface/ToggleToolFavoritedRepository.swift @@ -37,10 +37,6 @@ class ToggleToolFavoritedRepository: ToggleToolFavoritedRepositoryInterface { else { return favoritedResourcesRepository.storeFavoritedResourcesPublisher(ids: [toolId]) - .catch { _ in - return Just([]) - .eraseToAnyPublisher() - } .map { _ in ToolIsFavoritedDomainModel(dataModelId: toolId, isFavorited: true) } diff --git a/godtools/App/Features/Favorites/DependencyContainer/FavoritesDataLayerDependencies.swift b/godtools/App/Features/Favorites/DependencyContainer/FavoritesDataLayerDependencies.swift index f352b59bc..1b4e16fda 100644 --- a/godtools/App/Features/Favorites/DependencyContainer/FavoritesDataLayerDependencies.swift +++ b/godtools/App/Features/Favorites/DependencyContainer/FavoritesDataLayerDependencies.swift @@ -46,6 +46,12 @@ class FavoritesDataLayerDependencies { ) } + func getReorderFavoritedToolRepository() -> ReorderFavoritedToolRepositoryInterface { + return ReorderFavoritedToolRepository( + favoritedResourcesRepository: coreDataLayer.getFavoritedResourcesRepository() + ) + } + func getToggleToolFavoritedRepository() -> ToggleToolFavoritedRepositoryInterface { return ToggleToolFavoritedRepository( favoritedResourcesRepository: coreDataLayer.getFavoritedResourcesRepository() diff --git a/godtools/App/Features/Favorites/DependencyContainer/FavoritesDomainLayerDependencies.swift b/godtools/App/Features/Favorites/DependencyContainer/FavoritesDomainLayerDependencies.swift index 47bb12738..a016cf4cf 100644 --- a/godtools/App/Features/Favorites/DependencyContainer/FavoritesDomainLayerDependencies.swift +++ b/godtools/App/Features/Favorites/DependencyContainer/FavoritesDomainLayerDependencies.swift @@ -23,6 +23,12 @@ class FavoritesDomainLayerDependencies { ) } + func getReorderFavoritedToolUseCase() -> ReorderFavoritedToolUseCase { + return ReorderFavoritedToolUseCase( + reorderFavoritedToolRepository: dataLayer.getReorderFavoritedToolRepository() + ) + } + func getToggleFavoritedToolUseCase() -> ToggleToolFavoritedUseCase { return ToggleToolFavoritedUseCase( toggleToolFavoritedRepository: dataLayer.getToggleToolFavoritedRepository() diff --git a/godtools/App/Features/Favorites/Domain/Entities/ReorderFavoritedToolDomainModel.swift b/godtools/App/Features/Favorites/Domain/Entities/ReorderFavoritedToolDomainModel.swift new file mode 100644 index 000000000..00e0e1aba --- /dev/null +++ b/godtools/App/Features/Favorites/Domain/Entities/ReorderFavoritedToolDomainModel.swift @@ -0,0 +1,22 @@ +// +// ReorderFavoritedToolDomainModel.swift +// godtools +// +// Created by Rachael Skeath on 3/28/25. +// Copyright © 2025 Cru. All rights reserved. +// + +import Foundation + +struct ReorderFavoritedToolDomainModel { + + let dataModelId: String + let position: Int +} + +extension ReorderFavoritedToolDomainModel: Identifiable { + + var id: String { + return dataModelId + } +} diff --git a/godtools/App/Features/Favorites/Domain/Interface/ReorderFavoritedToolRepositoryInterface.swift b/godtools/App/Features/Favorites/Domain/Interface/ReorderFavoritedToolRepositoryInterface.swift new file mode 100644 index 000000000..e906196d2 --- /dev/null +++ b/godtools/App/Features/Favorites/Domain/Interface/ReorderFavoritedToolRepositoryInterface.swift @@ -0,0 +1,15 @@ +// +// ReorderFavoritedToolRepositoryInterface.swift +// godtools +// +// Created by Rachael Skeath on 3/19/25. +// Copyright © 2025 Cru. All rights reserved. +// + +import Foundation +import Combine + +protocol ReorderFavoritedToolRepositoryInterface { + + func reorderFavoritedToolPubilsher(toolId: String, originalPosition: Int, newPosition: Int) -> AnyPublisher<[ReorderFavoritedToolDomainModel], Error> +} diff --git a/godtools/App/Features/Favorites/Domain/UseCases/ReorderFavoritedToolUseCase.swift b/godtools/App/Features/Favorites/Domain/UseCases/ReorderFavoritedToolUseCase.swift new file mode 100644 index 000000000..21d6cacf3 --- /dev/null +++ b/godtools/App/Features/Favorites/Domain/UseCases/ReorderFavoritedToolUseCase.swift @@ -0,0 +1,24 @@ +// +// ReorderFavoritedToolUseCase.swift +// godtools +// +// Created by Rachael Skeath on 3/19/25. +// Copyright © 2025 Cru. All rights reserved. +// + +import Foundation +import Combine + +class ReorderFavoritedToolUseCase { + + private let reorderFavoritedToolRepository: ReorderFavoritedToolRepositoryInterface + + init(reorderFavoritedToolRepository: ReorderFavoritedToolRepositoryInterface) { + self.reorderFavoritedToolRepository = reorderFavoritedToolRepository + } + + func reorderFavoritedToolPublisher(toolId: String, originalPosition: Int, newPosition: Int) -> AnyPublisher<[ReorderFavoritedToolDomainModel], Error> { + + return reorderFavoritedToolRepository.reorderFavoritedToolPubilsher(toolId: toolId, originalPosition: originalPosition, newPosition: newPosition) + } +} diff --git a/godtools/App/Features/Favorites/Presentation/AllYourFavoriteTools/AllYourFavoriteToolsView.swift b/godtools/App/Features/Favorites/Presentation/AllYourFavoriteTools/AllYourFavoriteToolsView.swift index 84c608ff4..12155355d 100644 --- a/godtools/App/Features/Favorites/Presentation/AllYourFavoriteTools/AllYourFavoriteToolsView.swift +++ b/godtools/App/Features/Favorites/Presentation/AllYourFavoriteTools/AllYourFavoriteToolsView.swift @@ -25,53 +25,47 @@ struct AllYourFavoriteToolsView: View { var body: some View { GeometryReader { geometry in - - PullToRefreshScrollView(showsIndicators: true) { - - VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 0) { + List { Text(viewModel.sectionTitle) .font(FontLibrary.sfProTextRegular.font(size: 22)) .foregroundColor(ColorPalette.gtGrey.color) - .padding(.top, 30) - .padding(.leading, contentHorizontalInsets) - - LazyVStack(alignment: .center, spacing: toolCardSpacing) { - - ForEach(viewModel.favoritedTools) { (tool: YourFavoritedToolDomainModel) in - - ToolCardView( - viewModel: viewModel.getToolViewModel(tool: tool), - geometry: geometry, - layout: .landscape, - showsCategory: true, - navButtonTitleHorizontalPadding: YourFavoriteToolsView.toolCardNavButtonTitleHorizontalPadding, - accessibility: .favoriteTool, - favoriteTappedClosure: { - - viewModel.unfavoriteToolTapped(tool: tool) - }, - toolDetailsTappedClosure: { - - viewModel.toolDetailsTapped(tool: tool) - }, - openToolTappedClosure: { - - viewModel.openToolTapped(tool: tool) - }, - toolTappedClosure: { - - viewModel.toolTapped(tool: tool) - } - ) - } + .listRowSeparator(.hidden) + + ForEach(viewModel.favoritedTools) { (tool: YourFavoritedToolDomainModel) in + ToolCardView( + viewModel: viewModel.getToolViewModel(tool: tool), + geometry: geometry, + layout: .landscape, + showsCategory: true, + navButtonTitleHorizontalPadding: YourFavoriteToolsView.toolCardNavButtonTitleHorizontalPadding, + accessibility: .favoriteTool, + favoriteTappedClosure: { + + viewModel.unfavoriteToolTapped(tool: tool) + }, + toolDetailsTappedClosure: { + + viewModel.toolDetailsTapped(tool: tool) + }, + openToolTappedClosure: { + + viewModel.openToolTapped(tool: tool) + }, + toolTappedClosure: { + + viewModel.toolTapped(tool: tool) + } + ) + .buttonStyle(.plain) + .listRowSeparator(.hidden) + } + .onMove { from, to in + viewModel.toolMoved(fromOffsets: from, toOffset: to) } - .padding([.top], toolCardSpacing) - .padding([.bottom], DashboardView.scrollViewBottomSpacingToTabBar) } - - } refreshHandler: { - + .listStyle(.plain) } } .navigationBarBackButtonHidden(true) @@ -95,6 +89,7 @@ struct AllYourFavoriteToolsView_Preview: PreviewProvider { viewAllYourFavoritedToolsUseCase: appDiContainer.feature.favorites.domainLayer.getViewAllYourFavoritedToolsUseCase(), getCurrentAppLanguageUseCase: appDiContainer.feature.appLanguage.domainLayer.getCurrentAppLanguageUseCase(), getToolIsFavoritedUseCase: appDiContainer.feature.favorites.domainLayer.getToolIsFavoritedUseCase(), + reorderFavoritedToolUseCase: appDiContainer.feature.favorites.domainLayer.getReorderFavoritedToolUseCase(), attachmentsRepository: appDiContainer.dataLayer.getAttachmentsRepository(), trackScreenViewAnalyticsUseCase: appDiContainer.domainLayer.getTrackScreenViewAnalyticsUseCase(), trackActionAnalyticsUseCase: appDiContainer.domainLayer.getTrackActionAnalyticsUseCase() diff --git a/godtools/App/Features/Favorites/Presentation/AllYourFavoriteTools/AllYourFavoriteToolsViewModel.swift b/godtools/App/Features/Favorites/Presentation/AllYourFavoriteTools/AllYourFavoriteToolsViewModel.swift index 9d7e346d0..00ba280db 100644 --- a/godtools/App/Features/Favorites/Presentation/AllYourFavoriteTools/AllYourFavoriteToolsViewModel.swift +++ b/godtools/App/Features/Favorites/Presentation/AllYourFavoriteTools/AllYourFavoriteToolsViewModel.swift @@ -15,11 +15,13 @@ class AllYourFavoriteToolsViewModel: ObservableObject { private let viewAllYourFavoritedToolsUseCase: ViewAllYourFavoritedToolsUseCase private let getCurrentAppLanguageUseCase: GetCurrentAppLanguageUseCase private let getToolIsFavoritedUseCase: GetToolIsFavoritedUseCase + private let reorderFavoritedToolUseCase: ReorderFavoritedToolUseCase private let attachmentsRepository: AttachmentsRepository private let trackScreenViewAnalyticsUseCase: TrackScreenViewAnalyticsUseCase private let trackActionAnalyticsUseCase: TrackActionAnalyticsUseCase private let didConfirmToolRemovalSubject: PassthroughSubject = PassthroughSubject() + private static var backgroundCancellables: Set = Set() private var cancellables: Set = Set() private weak var flowDelegate: FlowDelegate? @@ -29,12 +31,13 @@ class AllYourFavoriteToolsViewModel: ObservableObject { @Published var sectionTitle: String = "" @Published var favoritedTools: [YourFavoritedToolDomainModel] = Array() - init(flowDelegate: FlowDelegate?, viewAllYourFavoritedToolsUseCase: ViewAllYourFavoritedToolsUseCase, getCurrentAppLanguageUseCase: GetCurrentAppLanguageUseCase, getToolIsFavoritedUseCase: GetToolIsFavoritedUseCase, attachmentsRepository: AttachmentsRepository, trackScreenViewAnalyticsUseCase: TrackScreenViewAnalyticsUseCase, trackActionAnalyticsUseCase: TrackActionAnalyticsUseCase) { + init(flowDelegate: FlowDelegate?, viewAllYourFavoritedToolsUseCase: ViewAllYourFavoritedToolsUseCase, getCurrentAppLanguageUseCase: GetCurrentAppLanguageUseCase, getToolIsFavoritedUseCase: GetToolIsFavoritedUseCase, reorderFavoritedToolUseCase: ReorderFavoritedToolUseCase, attachmentsRepository: AttachmentsRepository, trackScreenViewAnalyticsUseCase: TrackScreenViewAnalyticsUseCase, trackActionAnalyticsUseCase: TrackActionAnalyticsUseCase) { self.flowDelegate = flowDelegate self.viewAllYourFavoritedToolsUseCase = viewAllYourFavoritedToolsUseCase self.getCurrentAppLanguageUseCase = getCurrentAppLanguageUseCase self.getToolIsFavoritedUseCase = getToolIsFavoritedUseCase + self.reorderFavoritedToolUseCase = reorderFavoritedToolUseCase self.attachmentsRepository = attachmentsRepository self.trackScreenViewAnalyticsUseCase = trackScreenViewAnalyticsUseCase self.trackActionAnalyticsUseCase = trackActionAnalyticsUseCase @@ -190,4 +193,28 @@ extension AllYourFavoriteToolsViewModel { flowDelegate?.navigate(step: .toolTappedFromAllYourFavoritedTools(tool: tool)) } + + func toolMoved(fromOffsets source: IndexSet, toOffset destination: Int) { + for index in source { + guard index < favoritedTools.count else { continue } + let toolToMove = favoritedTools[index] + + var newIndex: Int + if index < destination { + newIndex = destination - 1 + } else { + newIndex = destination + } + + reorderFavoritedToolUseCase + .reorderFavoritedToolPublisher(toolId: toolToMove.id, originalPosition: index, newPosition: newIndex) + .sink { _ in + + } receiveValue: { _ in + + } + .store(in: &AllYourFavoriteToolsViewModel.backgroundCancellables) + + } + } } diff --git a/godtools/App/Features/ToolShortcutLinks/Data-DomainInterface/GetToolShortcutLinksRepository.swift b/godtools/App/Features/ToolShortcutLinks/Data-DomainInterface/GetToolShortcutLinksRepository.swift index cb5708f4f..e6378520a 100644 --- a/godtools/App/Features/ToolShortcutLinks/Data-DomainInterface/GetToolShortcutLinksRepository.swift +++ b/godtools/App/Features/ToolShortcutLinks/Data-DomainInterface/GetToolShortcutLinksRepository.swift @@ -25,7 +25,7 @@ class GetToolShortcutLinksRepository: GetToolShortcutLinksRepositoryInterface { func getLinksPublisher(appLanguage: AppLanguageDomainModel) -> AnyPublisher<[ToolShortcutLinkDomainModel], Never> { - return favoritedResourcesRepository.getFavoritedResourcesSortedByCreatedAtPublisher(ascendingOrder: false) + return favoritedResourcesRepository.getFavoritedResourcesSortedByPositionPublisher() .flatMap({ (favoritedResources: [FavoritedResourceDataModel]) -> AnyPublisher<[ToolShortcutLinkDomainModel], Never> in let toolShortcutLinks: [ToolShortcutLinkDomainModel] = favoritedResources diff --git a/godtools/App/Flows/App/AppFlow.swift b/godtools/App/Flows/App/AppFlow.swift index 7863391e7..f346a8825 100644 --- a/godtools/App/Flows/App/AppFlow.swift +++ b/godtools/App/Flows/App/AppFlow.swift @@ -947,6 +947,7 @@ extension AppFlow { viewAllYourFavoritedToolsUseCase: appDiContainer.feature.favorites.domainLayer.getViewAllYourFavoritedToolsUseCase(), getCurrentAppLanguageUseCase: appDiContainer.feature.appLanguage.domainLayer.getCurrentAppLanguageUseCase(), getToolIsFavoritedUseCase: appDiContainer.feature.favorites.domainLayer.getToolIsFavoritedUseCase(), + reorderFavoritedToolUseCase: appDiContainer.feature.favorites.domainLayer.getReorderFavoritedToolUseCase(), attachmentsRepository: appDiContainer.dataLayer.getAttachmentsRepository(), trackScreenViewAnalyticsUseCase: appDiContainer.domainLayer.getTrackScreenViewAnalyticsUseCase(), trackActionAnalyticsUseCase: appDiContainer.domainLayer.getTrackActionAnalyticsUseCase() diff --git a/godtools/App/Share/Data/FavoritedResourcesRepository/Cache/Models/RealmFavoritedResource.swift b/godtools/App/Share/Data/FavoritedResourcesRepository/Cache/Models/RealmFavoritedResource.swift index f7757d382..dfa90fd30 100644 --- a/godtools/App/Share/Data/FavoritedResourcesRepository/Cache/Models/RealmFavoritedResource.swift +++ b/godtools/App/Share/Data/FavoritedResourcesRepository/Cache/Models/RealmFavoritedResource.swift @@ -13,14 +13,9 @@ class RealmFavoritedResource: Object { @objc dynamic var createdAt: Date = Date() @objc dynamic var resourceId: String = "" + @objc dynamic var position: Int = 0 override static func primaryKey() -> String? { return "resourceId" } - - func mapFrom(dataModel: FavoritedResourceDataModel) { - - createdAt = dataModel.createdAt - resourceId = dataModel.id - } } diff --git a/godtools/App/Share/Data/FavoritedResourcesRepository/Cache/RealmFavoritedResourcesCache.swift b/godtools/App/Share/Data/FavoritedResourcesRepository/Cache/RealmFavoritedResourcesCache.swift index 90e888e56..dea2ce22c 100644 --- a/godtools/App/Share/Data/FavoritedResourcesRepository/Cache/RealmFavoritedResourcesCache.swift +++ b/godtools/App/Share/Data/FavoritedResourcesRepository/Cache/RealmFavoritedResourcesCache.swift @@ -56,28 +56,28 @@ class RealmFavoritedResourcesCache { .object(ofType: RealmFavoritedResource.self, forPrimaryKey: id) != nil } - func getFavoritedResourcesSortedByCreatedAt(ascendingOrder: Bool) -> [FavoritedResourceDataModel] { + func getFavoritedResourcesSortedByPosition() -> [FavoritedResourceDataModel] { - return realmDatabase.openRealm() - .objects(RealmFavoritedResource.self) - .sorted(byKeyPath: #keyPath(RealmFavoritedResource.createdAt), ascending: ascendingOrder) + return getFavoritesSortedByPosition() .map({FavoritedResourceDataModel(realmFavoritedResource: $0)}) } - func getFavoritedResourcesSortedByCreatedAtPublisher(ascendingOrder: Bool) -> AnyPublisher<[FavoritedResourceDataModel], Never> { + func getFavoritedResourcesSortedByPositionPublisher() -> AnyPublisher<[FavoritedResourceDataModel], Never> { - let favoritedResources: [FavoritedResourceDataModel] = realmDatabase.openRealm() - .objects(RealmFavoritedResource.self) - .sorted(byKeyPath: #keyPath(RealmFavoritedResource.createdAt), ascending: ascendingOrder) - .map({ - return FavoritedResourceDataModel(realmFavoritedResource: $0) - }) - - return Just(favoritedResources) + return getFavoritedResourcesChangedPublisher() + .flatMap { _ in + + let favoritedResources = self.getFavoritesSortedByPosition() + .map { FavoritedResourceDataModel(realmFavoritedResource: $0) } + + return Just(favoritedResources) + + } .eraseToAnyPublisher() + } - - func storeFavoritedResourcesPublisher(ids: [String]) -> AnyPublisher<[FavoritedResourceDataModel], Error> { + + func storeFavoritedResourcesPublisher(ids: [String]) -> AnyPublisher { let currentDate: Date = Date() let calendar: Calendar = Calendar.current @@ -90,24 +90,98 @@ class RealmFavoritedResourcesCache { continue } - let favoritedResource = FavoritedResourceDataModel(id: ids[index], createdAt: createdAtDate) + let favoritedResource = FavoritedResourceDataModel(id: ids[index], createdAt: createdAtDate, position: index) newFavoritedResources.append(favoritedResource) } return realmDatabase.writeObjectsPublisher { (realm: Realm) -> [RealmFavoritedResource] in - let realmFavoritedResources: [RealmFavoritedResource] = newFavoritedResources.map { + let existingFavorites = realm.objects(RealmFavoritedResource.self) + for favorite in existingFavorites { + favorite.position += newFavoritedResources.count + } + + let realmNewResources: [RealmFavoritedResource] = newFavoritedResources.map { let realmFavoritedResource = RealmFavoritedResource() - realmFavoritedResource.mapFrom(dataModel: $0) + realmFavoritedResource.resourceId = $0.id + realmFavoritedResource.createdAt = $0.createdAt + realmFavoritedResource.position = $0.position return realmFavoritedResource } - return realmFavoritedResources + return realmNewResources + Array(existingFavorites) - } mapInBackgroundClosure: { (objects: [RealmFavoritedResource]) -> [FavoritedResourceDataModel] in + } mapInBackgroundClosure: { (objects: [RealmFavoritedResource]) -> [Any] in + return [] + } + .catch({ error in + print(error) + return Just([]) + }) + .map { _ in + return () + } + .eraseToAnyPublisher() + } + + func deleteFavoritedResourcePublisher(id: String) -> AnyPublisher { + + return realmDatabase.writeObjectsPublisher { realm in + + guard let positionToDelete = realm.object(ofType: RealmFavoritedResource.self, forPrimaryKey: id)?.position else { return [] } + + let resourcesToMoveUp = realm.objects(RealmFavoritedResource.self).where({ $0.position >= positionToDelete }) + for resource in resourcesToMoveUp { + resource.position -= 1 + } + + return Array(resourcesToMoveUp) + + } mapInBackgroundClosure: { objects in + return [] + } + .flatMap { _ in + return self.realmDatabase.deleteObjectsInBackgroundPublisher( + type: RealmFavoritedResource.self, + primaryKeyPath: #keyPath(RealmFavoritedResource.resourceId), + primaryKeys: [id] + ) + } + .eraseToAnyPublisher() + } + + func reorderFavoritedResourcePublisher(id: String, originalPosition: Int, newPosition: Int) -> AnyPublisher<[FavoritedResourceDataModel], Error> { + + return realmDatabase.writeObjectsPublisher { realm in + var resourcesToUpdate: [RealmFavoritedResource] = [] + + if newPosition - originalPosition > 0 { + let resourcesToMoveUp = realm.objects(RealmFavoritedResource.self).where({ $0.position >= originalPosition && $0.position <= newPosition }) + for resource in resourcesToMoveUp { + resource.position -= 1 + } + + resourcesToUpdate += resourcesToMoveUp + } else { + let resourcesToMoveDown = realm.objects(RealmFavoritedResource.self).where( { $0.position <= originalPosition && $0.position >= newPosition}) + for resource in resourcesToMoveDown { + resource.position += 1 + } + + resourcesToUpdate += resourcesToMoveDown + } + + if let resourceToMove = realm.object(ofType: RealmFavoritedResource.self, forPrimaryKey: id) { + resourceToMove.position = newPosition + resourcesToUpdate.append(resourceToMove) + } + + return resourcesToUpdate + + } mapInBackgroundClosure: { (objects: [RealmFavoritedResource]) in return objects.map({ FavoritedResourceDataModel(realmFavoritedResource: $0) }) @@ -115,13 +189,45 @@ class RealmFavoritedResourcesCache { .eraseToAnyPublisher() } - func deleteFavoritedResourcePublisher(id: String) -> AnyPublisher { + // MARK: - Private + + private func getFavoritesSortedByPosition() -> [RealmFavoritedResource] { + let realm = realmDatabase.openRealm() + let favoritesSortedByPosition = Array( + realm.objects(RealmFavoritedResource.self) + .sorted(byKeyPath: #keyPath(RealmFavoritedResource.position), ascending: true) + ) - return realmDatabase.deleteObjectsInBackgroundPublisher( - type: RealmFavoritedResource.self, - primaryKeyPath: #keyPath(RealmFavoritedResource.resourceId), - primaryKeys: [id] - ) - .eraseToAnyPublisher() + if favoritesSortedByPosition.allSatisfy({ $0.position == 0 }) { + + return migrateExistingFavoritesToPositions() + + } else { + return favoritesSortedByPosition + } + } + + @available(*, deprecated) + private func migrateExistingFavoritesToPositions() -> [RealmFavoritedResource] { + let realm = realmDatabase.openRealm() + let favoritesSortedByCreatedAt = realm + .objects(RealmFavoritedResource.self) + .sorted(byKeyPath: #keyPath(RealmFavoritedResource.createdAt), ascending: false) + + do { + try realm.write { + var correctedPosition = 0 + for favorite in favoritesSortedByCreatedAt { + favorite.position = correctedPosition + correctedPosition += 1 + } + realm.add(favoritesSortedByCreatedAt) + } + } + catch let error { + print(error) + } + + return Array(favoritesSortedByCreatedAt) } } diff --git a/godtools/App/Share/Data/FavoritedResourcesRepository/FavoritedResourceDataModel.swift b/godtools/App/Share/Data/FavoritedResourcesRepository/FavoritedResourceDataModel.swift index dbc3df56a..a97e7478e 100644 --- a/godtools/App/Share/Data/FavoritedResourcesRepository/FavoritedResourceDataModel.swift +++ b/godtools/App/Share/Data/FavoritedResourcesRepository/FavoritedResourceDataModel.swift @@ -12,22 +12,26 @@ struct FavoritedResourceDataModel { let createdAt: Date let id: String + let position: Int init(id: String) { self.createdAt = Date() self.id = id + self.position = 0 } - init(id: String, createdAt: Date) { + init(id: String, createdAt: Date, position: Int) { self.createdAt = createdAt self.id = id + self.position = position } init(realmFavoritedResource: RealmFavoritedResource) { self.createdAt = realmFavoritedResource.createdAt self.id = realmFavoritedResource.resourceId + self.position = realmFavoritedResource.position } } diff --git a/godtools/App/Share/Data/FavoritedResourcesRepository/FavoritedResourcesRepository.swift b/godtools/App/Share/Data/FavoritedResourcesRepository/FavoritedResourcesRepository.swift index a3fb6cdef..7c30c1b57 100644 --- a/godtools/App/Share/Data/FavoritedResourcesRepository/FavoritedResourcesRepository.swift +++ b/godtools/App/Share/Data/FavoritedResourcesRepository/FavoritedResourcesRepository.swift @@ -46,19 +46,19 @@ class FavoritedResourcesRepository { func getResourceIsFavorited(id: String) -> Bool { return cache.getResourceIsFavorited(id: id) } - - func getFavoritedResourcesSortedByCreatedAt(ascendingOrder: Bool) -> [FavoritedResourceDataModel] { + + func getFavoritedResourcesSortedByPosition() -> [FavoritedResourceDataModel] { - return cache.getFavoritedResourcesSortedByCreatedAt(ascendingOrder: ascendingOrder) + return cache.getFavoritedResourcesSortedByPosition() } - func getFavoritedResourcesSortedByCreatedAtPublisher(ascendingOrder: Bool) -> AnyPublisher<[FavoritedResourceDataModel], Never> { + func getFavoritedResourcesSortedByPositionPublisher() -> AnyPublisher<[FavoritedResourceDataModel], Never> { - return cache.getFavoritedResourcesSortedByCreatedAtPublisher(ascendingOrder: ascendingOrder) + return cache.getFavoritedResourcesSortedByPositionPublisher() .eraseToAnyPublisher() } - func storeFavoritedResourcesPublisher(ids: [String]) -> AnyPublisher<[FavoritedResourceDataModel], Error> { + func storeFavoritedResourcesPublisher(ids: [String]) -> AnyPublisher { return cache.storeFavoritedResourcesPublisher(ids: ids) .eraseToAnyPublisher() @@ -69,4 +69,9 @@ class FavoritedResourcesRepository { return cache.deleteFavoritedResourcePublisher(id: id) .eraseToAnyPublisher() } + + func reorderFavoritedResourcePublisher(id: String, originalPosition: Int, newPosition: Int) -> AnyPublisher<[FavoritedResourceDataModel], Error> { + + return cache.reorderFavoritedResourcePublisher(id: id, originalPosition: originalPosition, newPosition: newPosition) + } } diff --git a/godtools/App/Share/Data/RealmDatabase/Configuration/RealmDatabaseProductionConfiguration.swift b/godtools/App/Share/Data/RealmDatabase/Configuration/RealmDatabaseProductionConfiguration.swift index cac9ff241..c7f34399e 100644 --- a/godtools/App/Share/Data/RealmDatabase/Configuration/RealmDatabaseProductionConfiguration.swift +++ b/godtools/App/Share/Data/RealmDatabase/Configuration/RealmDatabaseProductionConfiguration.swift @@ -12,7 +12,7 @@ import RealmSwift class RealmDatabaseProductionConfiguration: RealmDatabaseConfiguration { private static let diskFileName: String = "godtools_realm" - private static let schemaVersion: UInt64 = 35 + private static let schemaVersion: UInt64 = 36 init() { diff --git a/godtoolsTests/App/Features/Favorites/Data-DomainInterface/FavoritedResourcesRepositoryTests.swift b/godtoolsTests/App/Features/Favorites/Data-DomainInterface/FavoritedResourcesRepositoryTests.swift new file mode 100644 index 000000000..32345c352 --- /dev/null +++ b/godtoolsTests/App/Features/Favorites/Data-DomainInterface/FavoritedResourcesRepositoryTests.swift @@ -0,0 +1,67 @@ +// +// FavoritedResourcesRepositoryTests.swift +// godtoolsTests +// +// Created by Rachael Skeath on 4/5/25. +// Copyright © 2025 Cru. All rights reserved. +// + +import Foundation +@testable import godtools +import Combine +import Quick +import Nimble + +class FavoritedResourcesRepositoryTests: QuickSpec { + + override class func spec() { + + var cancellables: Set = Set() + + describe("User has only tools A and B favorited.") { + + context("Tool C, D, and E are all added to favorites simultaneously") { + it("Tool C, D, and E should get added to favorites at position 0, 1, and 2. Positions of tools A and B should update to 3 and 4 respectively") { + + let realmDatabase = getConfiguredRealmDatabase() + let favoritedResourcesRepository = FavoritedResourcesRepository(cache: RealmFavoritedResourcesCache(realmDatabase: realmDatabase)) + + var favoritedResources: [FavoritedResourceDataModel] = Array() + + waitUntil{ done in + favoritedResourcesRepository.storeFavoritedResourcesPublisher(ids: ["C", "D", "E"]) + .sink(receiveValue: { _ in + + favoritedResources = realmDatabase.openRealm().objects(RealmFavoritedResource.self).map { + FavoritedResourceDataModel(id: $0.resourceId, createdAt: $0.createdAt, position: $0.position) + } + + done() + }) + .store(in: &cancellables) + } + + expect(favoritedResources.first(where: { $0.id == "C" })?.position).to(equal(0)) + expect(favoritedResources.first(where: { $0.id == "D" })?.position).to(equal(1)) + expect(favoritedResources.first(where: { $0.id == "E" })?.position).to(equal(2)) + expect(favoritedResources.first(where: { $0.id == "A" })?.position).to(equal(3)) + expect(favoritedResources.first(where: { $0.id == "B" })?.position).to(equal(4)) + } + } + } + } + + private class func getConfiguredRealmDatabase() -> RealmDatabase { + let favoriteResourceA = RealmFavoritedResource() + favoriteResourceA.resourceId = "A" + favoriteResourceA.position = 0 + + let favoriteResourceB = RealmFavoritedResource() + favoriteResourceB.resourceId = "B" + favoriteResourceB.position = 1 + + return TestsInMemoryRealmDatabase(addObjectsToDatabase: [favoriteResourceA, favoriteResourceB] ) + } +} + + diff --git a/godtoolsTests/App/Features/Favorites/Data-DomainInterface/RemoveFavoritedToolRepositoryTests.swift b/godtoolsTests/App/Features/Favorites/Data-DomainInterface/RemoveFavoritedToolRepositoryTests.swift new file mode 100644 index 000000000..bdacb79b0 --- /dev/null +++ b/godtoolsTests/App/Features/Favorites/Data-DomainInterface/RemoveFavoritedToolRepositoryTests.swift @@ -0,0 +1,79 @@ +// +// RemoveFavoritedToolRepositoryTests.swift +// godtoolsTests +// +// Created by Rachael Skeath on 3/28/25. +// Copyright © 2025 Cru. All rights reserved. +// + +import Foundation +@testable import godtools +import Combine +import Quick +import Nimble + +class RemoveFavoritedToolRepositoryTests: QuickSpec { + + override class func spec() { + + var cancellables: Set = Set() + + describe("User is viewing all their favorite tools.") { + + context("When a user unfavorites tool B") { + it("Tools C, D, and E should update to positions 1, 2, and 3. Tool A should remain unchanged.") { + + let realmDatabase = getConfiguredRealmDatabase() + let removeFavoritedToolRepository = RemoveFavoritedToolRepository(favoritedResourcesRepository: FavoritedResourcesRepository(cache: RealmFavoritedResourcesCache(realmDatabase: realmDatabase))) + + var remainingResources: [FavoritedResourceDataModel] = Array() + + waitUntil{ done in + removeFavoritedToolRepository.removeToolPublisher(toolId: "B") + .sink(receiveValue: { _ in + + remainingResources = realmDatabase.openRealm().objects(RealmFavoritedResource.self).map { + FavoritedResourceDataModel(id: $0.resourceId, createdAt: $0.createdAt, position: $0.position) + } + + done() + }) + .store(in: &cancellables) + } + + expect(remainingResources.first(where: { $0.id == "A" })?.position).to(equal(0)) + expect(remainingResources.first(where: { $0.id == "B" })).to(beNil()) + expect(remainingResources.first(where: { $0.id == "C" })?.position).to(equal(1)) + expect(remainingResources.first(where: { $0.id == "D" })?.position).to(equal(2)) + expect(remainingResources.first(where: { $0.id == "E" })?.position).to(equal(3)) + } + } + + } + } + + private class func getConfiguredRealmDatabase() -> RealmDatabase { + let favoriteResourceA = RealmFavoritedResource() + favoriteResourceA.resourceId = "A" + favoriteResourceA.position = 0 + + let favoriteResourceB = RealmFavoritedResource() + favoriteResourceB.resourceId = "B" + favoriteResourceB.position = 1 + + let favoriteResourceC = RealmFavoritedResource() + favoriteResourceC.resourceId = "C" + favoriteResourceC.position = 2 + + let favoriteResourceD = RealmFavoritedResource() + favoriteResourceD.resourceId = "D" + favoriteResourceD.position = 3 + + let favoriteResourceE = RealmFavoritedResource() + favoriteResourceE.resourceId = "E" + favoriteResourceE.position = 4 + + return TestsInMemoryRealmDatabase(addObjectsToDatabase: [favoriteResourceA, favoriteResourceB, favoriteResourceC, favoriteResourceD, favoriteResourceE] ) + } +} + diff --git a/godtoolsTests/App/Features/Favorites/Data-DomainInterface/ReorderFavoritedToolRepositoryTests.swift b/godtoolsTests/App/Features/Favorites/Data-DomainInterface/ReorderFavoritedToolRepositoryTests.swift new file mode 100644 index 000000000..7fa00c809 --- /dev/null +++ b/godtoolsTests/App/Features/Favorites/Data-DomainInterface/ReorderFavoritedToolRepositoryTests.swift @@ -0,0 +1,137 @@ +// +// ReorderFavoritedToolRepositoryTests.swift +// godtoolsTests +// +// Created by Rachael Skeath on 3/21/25. +// Copyright © 2025 Cru. All rights reserved. +// + +import Foundation +@testable import godtools +import Combine +import Quick +import Nimble + +class ReorderFavoritedToolRepositoryTests: QuickSpec { + + override class func spec() { + + var cancellables: Set = Set() + + describe("User is viewing all their favorite tools.") { + + context("When the user drags tool C to the top") { + it("The tool C position should update to 0, tools A and B should update to 1 and 2, but D and E shouldn't change.") { + + let reorderFavoriteToolsRepository = ReorderFavoritedToolRepository( + favoritedResourcesRepository: FavoritedResourcesRepository(cache: RealmFavoritedResourcesCache(realmDatabase: getConfiguredRealmDatabase()))) + + var favoritedResources: [ReorderFavoritedToolDomainModel] = Array() + + waitUntil { done in + + reorderFavoriteToolsRepository.reorderFavoritedToolPubilsher(toolId: "C", originalPosition: 2, newPosition: 0) + .sink { _ in + + } receiveValue: { favorites in + + favoritedResources = favorites + done() + } + .store(in: &cancellables) + } + + expect(favoritedResources.count).to(equal(3)) + + expect(favoritedResources.first(where: { $0.id == "C" })?.position).to(equal(0)) + expect(favoritedResources.first(where: { $0.id == "A" })?.position).to(equal(1)) + expect(favoritedResources.first(where: { $0.id == "B" })?.position).to(equal(2)) + } + } + + context("When the user drags tool A to the bottom") { + it("The tool A position should update to 4 and B, C, D, E should update to 0, 1, 2, 3.") { + + let reorderFavoriteToolsRepository = ReorderFavoritedToolRepository( + favoritedResourcesRepository: FavoritedResourcesRepository(cache: RealmFavoritedResourcesCache(realmDatabase: getConfiguredRealmDatabase()))) + + var favoritedResources: [ReorderFavoritedToolDomainModel] = Array() + + waitUntil { done in + + reorderFavoriteToolsRepository.reorderFavoritedToolPubilsher(toolId: "A", originalPosition: 0, newPosition: 4) + .sink { _ in + + } receiveValue: { favorites in + + favoritedResources = favorites + done() + } + .store(in: &cancellables) + } + + expect(favoritedResources.count).to(equal(5)) + + expect(favoritedResources.first(where: { $0.id == "A" })?.position).to(equal(4)) + expect(favoritedResources.first(where: { $0.id == "B" })?.position).to(equal(0)) + expect(favoritedResources.first(where: { $0.id == "C" })?.position).to(equal(1)) + expect(favoritedResources.first(where: { $0.id == "D" })?.position).to(equal(2)) + expect(favoritedResources.first(where: { $0.id == "E" })?.position).to(equal(3)) + } + } + + context("When the user drags tool E to the third position") { + it("The tool E position should update to 2. A and B should remain unchanged. C and D should update to 3, 4.") { + + let reorderFavoriteToolsRepository = ReorderFavoritedToolRepository( + favoritedResourcesRepository: FavoritedResourcesRepository(cache: RealmFavoritedResourcesCache(realmDatabase: getConfiguredRealmDatabase()))) + + var favoritedResources: [ReorderFavoritedToolDomainModel] = Array() + + waitUntil { done in + + reorderFavoriteToolsRepository.reorderFavoritedToolPubilsher(toolId: "E", originalPosition: 4, newPosition: 2) + .sink { _ in + + } receiveValue: { favorites in + + favoritedResources = favorites + done() + } + .store(in: &cancellables) + } + + expect(favoritedResources.count).to(equal(3)) + + expect(favoritedResources.first(where: { $0.id == "E" })?.position).to(equal(2)) + expect(favoritedResources.first(where: { $0.id == "C" })?.position).to(equal(3)) + expect(favoritedResources.first(where: { $0.id == "D" })?.position).to(equal(4)) + } + } + } + } + + private class func getConfiguredRealmDatabase() -> RealmDatabase { + let favoriteResourceA = RealmFavoritedResource() + favoriteResourceA.resourceId = "A" + favoriteResourceA.position = 0 + + let favoriteResourceB = RealmFavoritedResource() + favoriteResourceB.resourceId = "B" + favoriteResourceB.position = 1 + + let favoriteResourceC = RealmFavoritedResource() + favoriteResourceC.resourceId = "C" + favoriteResourceC.position = 2 + + let favoriteResourceD = RealmFavoritedResource() + favoriteResourceD.resourceId = "D" + favoriteResourceD.position = 3 + + let favoriteResourceE = RealmFavoritedResource() + favoriteResourceE.resourceId = "E" + favoriteResourceE.position = 4 + + return TestsInMemoryRealmDatabase(addObjectsToDatabase: [favoriteResourceA, favoriteResourceB, favoriteResourceC, favoriteResourceD, favoriteResourceE] ) + } +} diff --git a/godtoolsTests/App/Features/Favorites/Data-DomainInterface/ToggleToolFavoritedRepositoryTests.swift b/godtoolsTests/App/Features/Favorites/Data-DomainInterface/ToggleToolFavoritedRepositoryTests.swift new file mode 100644 index 000000000..6d4a4317a --- /dev/null +++ b/godtoolsTests/App/Features/Favorites/Data-DomainInterface/ToggleToolFavoritedRepositoryTests.swift @@ -0,0 +1,70 @@ +// +// ToggleToolFavoritedRepositoryTests.swift +// godtoolsTests +// +// Created by Rachael Skeath on 3/28/25. +// Copyright © 2025 Cru. All rights reserved. +// + +import Foundation +@testable import godtools +import Combine +import Quick +import Nimble + +class ToggleToolFavoritedRepositoryTests: QuickSpec { + + override class func spec() { + + var cancellables: Set = Set() + + describe("User has only tools A and B favorited.") { + + context("When a user favorites tool C") { + it("Tool C should get added to favorites at position 0. Positions of tools A and B should update to 1 and 2 respectively") { + + let realmDatabase = getConfiguredRealmDatabase() + let toggleToolFavoritedRepository = ToggleToolFavoritedRepository(favoritedResourcesRepository: FavoritedResourcesRepository(cache: RealmFavoritedResourcesCache(realmDatabase: realmDatabase))) + + var favoritedResources: [FavoritedResourceDataModel] = Array() + + waitUntil{ done in + toggleToolFavoritedRepository.toggleFavoritedPublisher(toolId: "C") + .sink { _ in + + favoritedResources = realmDatabase.openRealm().objects(RealmFavoritedResource.self).map { + FavoritedResourceDataModel(id: $0.resourceId, createdAt: $0.createdAt, position: $0.position) + } + + done() + } + .store(in: &cancellables) + } + + expect(favoritedResources.first(where: { $0.id == "C" })?.position).to(equal(0)) + expect(favoritedResources.first(where: { $0.id == "A" })?.position).to(equal(1)) + expect(favoritedResources.first(where: { $0.id == "B" })?.position).to(equal(2)) + } + } + + } + } + + private class func getConfiguredRealmDatabase() -> RealmDatabase { + let favoriteResourceA = RealmFavoritedResource() + favoriteResourceA.resourceId = "A" + favoriteResourceA.position = 0 + + let favoriteResourceB = RealmFavoritedResource() + favoriteResourceB.resourceId = "B" + favoriteResourceB.position = 1 + + return Self.getConfiguredRealmDatabase(includeFavoritedTools: [favoriteResourceA, favoriteResourceB]) + } + + private class func getConfiguredRealmDatabase(includeFavoritedTools: [RealmFavoritedResource]) -> RealmDatabase { + + return TestsInMemoryRealmDatabase(addObjectsToDatabase: includeFavoritedTools) + } +} +