From 5ae27cbccb61751e7700ab3570e16a8faf1c4447 Mon Sep 17 00:00:00 2001 From: Emin Date: Wed, 18 Sep 2024 22:59:57 +0500 Subject: [PATCH] android: disable car support, sent to Play Store for review; ios: backup --- android/app/.gitignore | 3 + android/app/build.gradle | 10 +- android/app/src/main/AndroidManifest.xml | 34 +++--- .../app/tourism/data/db/dao/ReviewsDao.kt | 4 +- .../data/repositories/ReviewsRepository.kt | 6 +- .../ui/common/special/CountryAsLabel.kt | 1 + .../ui/screens/main/home/HomeScreen.kt | 2 +- .../ui/screens/main/home/HomeViewModel.kt | 2 - .../reviews/PostReviewViewModel.kt | 2 +- .../place_details/reviews/ReviewsViewModel.kt | 6 +- .../reviews/components/Review.kt | 21 ++-- .../main/profile/profile/ProfileViewModel.kt | 2 +- android/app/src/main/res/layout/ccp_auth.xml | 4 +- .../app/src/main/res/values-ru/strings.xml | 2 +- android/app/src/main/res/values/strings.xml | 2 +- .../en-GB.lproj/Localizable.strings | 4 +- .../en.lproj/Localizable.strings | 4 +- .../ru.lproj/Localizable.strings | 4 +- iphone/Maps/Maps.xcodeproj/project.pbxproj | 36 +++---- .../Place.xcdatamodel/contents | 5 +- .../Data/Db/DataModels/UserEntity.swift | 2 +- .../Tourism/Data/Db/EntitiesMapping.swift | 35 ++++++ .../PlacesPersistenceController.swift | 23 ++-- .../ReviewsPersistenceController.swift | 102 ++++++++++-------- .../Network/DTO/Details/ReviewIdsDTO.swift | 5 - .../{ReviewDTO.swift => Reviews DTOs.swift} | 18 ++++ .../Data/Network/DTO/Details/ReviewsDTO.swift | 5 - .../Network/Services/ReviewsService.swift | 83 ++++++++++++-- .../Data/Network/Utils/NetworkHelper.swift | 2 +- .../Repositories/PlacesRepositoryImpl.swift | 36 ++++++- .../Repositories/ProfileRepositoryImpl.swift | 7 +- .../Repositories/ReviewsRepositoryImpl.swift | 102 +++++++++++++++--- iphone/Maps/Tourism/Data/ResourceError.swift | 3 + .../Domain/Models/Details/Review Models.swift | 24 +++++ .../Domain/Models/Details/Review.swift | 12 --- .../Domain/Models/Details/ReviewToPost.swift | 8 -- .../Repositories/ReviewsRepository.swift | 2 +- .../Auth/Screens/WelcomeViewController.swift | 2 +- .../Presentation/Components/LoadImg.swift | 29 +++-- .../Presentation/Home/DataSyncer.swift | 80 ++++++++++++++ .../Home/Screens/Home/HorizontalPlaces.swift | 91 ++++++++-------- .../Profile/PersonalDataViewController.swift | 4 +- .../Profile/PlaceDetails/AllPicsScreen.swift | 4 + .../PlaceDetails/PlaceViewController.swift | 22 +++- .../Reviews/AllReviewsScreen.swift | 4 + .../Reviews/Components/PostReviewView.swift | 85 +++++++++------ .../Reviews/Components/ReviewView.swift | 19 ++-- .../Reviews/PostReviewViewModel.swift | 70 ++++++------ .../PlaceDetails/Reviews/ReviewsScreen.swift | 55 +++++++--- .../Reviews/ReviewsViewModel.swift | 85 ++++++++++++--- .../Profile/ProfileViewController.swift | 1 + .../Screens/Profile/ProfileViewModel.swift | 3 +- .../Presentation/Home/TabBarController.swift | 11 ++ .../Maps/Tourism/Utils/ImageStoreUtils.swift | 33 ++++++ 54 files changed, 871 insertions(+), 350 deletions(-) delete mode 100644 iphone/Maps/Tourism/Data/Network/DTO/Details/ReviewIdsDTO.swift rename iphone/Maps/Tourism/Data/Network/DTO/Details/{ReviewDTO.swift => Reviews DTOs.swift} (63%) delete mode 100644 iphone/Maps/Tourism/Data/Network/DTO/Details/ReviewsDTO.swift create mode 100644 iphone/Maps/Tourism/Domain/Models/Details/Review Models.swift delete mode 100644 iphone/Maps/Tourism/Domain/Models/Details/Review.swift delete mode 100644 iphone/Maps/Tourism/Domain/Models/Details/ReviewToPost.swift create mode 100644 iphone/Maps/Tourism/Presentation/Home/DataSyncer.swift create mode 100644 iphone/Maps/Tourism/Utils/ImageStoreUtils.swift diff --git a/android/app/.gitignore b/android/app/.gitignore index 42b0b5086e..8d5c24fc9a 100644 --- a/android/app/.gitignore +++ b/android/app/.gitignore @@ -31,3 +31,6 @@ # ignore autogenerated metadata (see prepareGoogleReleaseListing in build.gradle) /src/google/play/listings + +# ignore google releases +/google/release diff --git a/android/app/build.gradle b/android/app/build.gradle index 7734b32239..d0dae35516 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -87,7 +87,7 @@ def getCommitMessage() { def osName = System.properties['os.name'].toLowerCase() project.ext.appId = 'tj.tourism.rebus' -project.ext.appName = 'Tourism' +project.ext.appName = 'Tourism Map Tajikistan' java { toolchain { @@ -111,10 +111,10 @@ android { defaultConfig { // Default package name is taken from the manifest and should be app.organicmaps def ver = getVersion() - versionCode = ver.V1 - versionName = ver.V2 - println('Version: ' + versionName) - println('VersionCode: ' + versionCode) + versionCode = 2 + versionName = "1.0.0" +// println('Version: ' + versionName) +// println('VersionCode: ' + versionCode) minSdk propMinSdkVersion.toInteger() targetSdk propTargetSdkVersion.toInteger() applicationId project.ext.appId diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 515395a38b..6e5c9a0c1a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -44,8 +44,8 @@ // --> - - + + @@ -444,16 +444,16 @@ android:label="@string/driving_options_title" /> - - - + + + + + + - - - + + + - - + + + + + + \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/db/dao/ReviewsDao.kt b/android/app/src/main/java/app/tourism/data/db/dao/ReviewsDao.kt index e0f384c753..5d5ab15eee 100644 --- a/android/app/src/main/java/app/tourism/data/db/dao/ReviewsDao.kt +++ b/android/app/src/main/java/app/tourism/data/db/dao/ReviewsDao.kt @@ -47,6 +47,6 @@ interface ReviewsDao { @Query("SELECT * FROM reviews_planned_to_post") fun getReviewsPlannedToPost(): List - @Query("SELECT * FROM reviews_planned_to_post") - fun getReviewsPlannedToPostFlow(): Flow> + @Query("SELECT * FROM reviews_planned_to_post WHERE placeId = :placeId") + fun getReviewsPlannedToPostFlow(placeId: Long): Flow> } diff --git a/android/app/src/main/java/app/tourism/data/repositories/ReviewsRepository.kt b/android/app/src/main/java/app/tourism/data/repositories/ReviewsRepository.kt index a8aee724fd..74fc582f8b 100644 --- a/android/app/src/main/java/app/tourism/data/repositories/ReviewsRepository.kt +++ b/android/app/src/main/java/app/tourism/data/repositories/ReviewsRepository.kt @@ -41,8 +41,8 @@ class ReviewsRepository( } } - fun isThereReviewPlannedToPublish(): Flow = channelFlow { - reviewsDao.getReviewsPlannedToPostFlow().collectLatest { reviewsEntities -> + fun isThereReviewPlannedToPublish(placeId: Long): Flow = channelFlow { + reviewsDao.getReviewsPlannedToPostFlow(placeId).collectLatest { reviewsEntities -> send(reviewsEntities.isNotEmpty()) } } @@ -84,7 +84,7 @@ class ReviewsRepository( try { saveToInternalStorage(imageFiles, context) reviewsDao.insertReviewPlannedToPost(review.toReviewPlannedToPostEntity(imageFiles)) - emit(Resource.Error(context.getString(R.string.review_will_be_published))) + emit(Resource.Error(context.getString(R.string.review_will_be_published_when_online))) } catch (e: OutOfMemoryError) { e.printStackTrace() emit(Resource.Error(context.getString(R.string.smth_went_wrong))) diff --git a/android/app/src/main/java/app/tourism/ui/common/special/CountryAsLabel.kt b/android/app/src/main/java/app/tourism/ui/common/special/CountryAsLabel.kt index 77eb1f9a73..4bef3e92b7 100644 --- a/android/app/src/main/java/app/tourism/ui/common/special/CountryAsLabel.kt +++ b/android/app/src/main/java/app/tourism/ui/common/special/CountryAsLabel.kt @@ -16,6 +16,7 @@ fun CountryAsLabel(modifier: Modifier = Modifier, countryCodeName: String, conte .inflate(R.layout.ccp_as_country_label, null, false) val ccp = view.findViewById(R.id.ccp) ccp.contentColor = contentColor + ccp.setCountryForNameCode("BO") ccp.setCountryForNameCode(countryCodeName) ccp.showArrow(false) ccp.setCcpClickable(false) diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeScreen.kt index 3b76bb5a47..58ddd75a58 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeScreen.kt @@ -105,7 +105,7 @@ fun HomeScreen( }, contentWindowInsets = WindowInsets(left = 0.dp, right = 0.dp, top = 0.dp, bottom = 0.dp) ) { paddingValues -> - if (downloadResponse is Resource.Success) + if (downloadResponse is Resource.Success || downloadResponse is Resource.Idle) Column( Modifier .padding(paddingValues) diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeViewModel.kt index 5bfa83b421..a45ec77197 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeViewModel.kt @@ -53,7 +53,6 @@ class HomeViewModel @Inject constructor( if (resource is Resource.Success) { resource.data?.let { _sights.value = it - Log.d("lok narosh", it.toString()) } } } @@ -73,7 +72,6 @@ class HomeViewModel @Inject constructor( .collectLatest { resource -> if (resource is Resource.Success) { resource.data?.let { - Log.d("lok narosh", it.toString()) _restaurants.value = it } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/PostReviewViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/PostReviewViewModel.kt index 0bb9a9c835..ff3f8a2c23 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/PostReviewViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/PostReviewViewModel.kt @@ -86,7 +86,7 @@ class PostReviewViewModel @Inject constructor( uiChannel.send( UiEvent.ShowToast(it.message ?: context.getString(R.string.smth_went_wrong)) ) - if (it.message == context.getString(R.string.review_will_be_published)) { + if (it.message == context.getString(R.string.review_will_be_published_when_online)) { uiChannel.send(UiEvent.CloseReviewBottomSheet) } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt index 9371836e63..98ae80d81a 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt @@ -70,8 +70,10 @@ class ReviewsViewModel @Inject constructor( init { viewModelScope.launch(Dispatchers.IO) { - reviewsRepository.isThereReviewPlannedToPublish().collectLatest { - _isThereReviewPlannedToPublish.value = it + userReview.value?.id?.let { placeId -> + reviewsRepository.isThereReviewPlannedToPublish(placeId).collectLatest { + _isThereReviewPlannedToPublish.value = it + } } } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt index e178a072c1..1ce0f9dd97 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio @@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider @@ -44,6 +46,7 @@ import app.tourism.ui.common.HorizontalSpace import app.tourism.ui.common.LoadImg import app.tourism.ui.common.VerticalSpace import app.tourism.ui.common.special.CountryAsLabel +import app.tourism.ui.common.special.CountryFlag import app.tourism.ui.common.special.RatingBar import app.tourism.ui.screens.main.place_details.gallery.imageShape import app.tourism.ui.theme.TextStyles @@ -138,20 +141,22 @@ fun User(modifier: Modifier = Modifier, user: User) { url = user.pfpUrl, ) HorizontalSpace(width = 12.dp) - Column { - VerticalSpace(height = 6.dp) + Row(modifier = Modifier) { + Column { + VerticalSpace(3.dp) + CountryFlag( + modifier = Modifier.width(50.dp), + countryCodeName = user.countryCodeName, + ) + } Text( + modifier = Modifier.weight(1f).width(IntrinsicSize.Min), text = user.name, style = TextStyles.h4, fontWeight = FontWeight.W600, - maxLines = 1, + maxLines = 2, overflow = TextOverflow.Ellipsis, ) - CountryAsLabel( - Modifier.fillMaxWidth(), - user.countryCodeName, - contentColor = MaterialTheme.colorScheme.onBackground.toArgb(), - ) } } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt index a96250368f..11a5048ae5 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt @@ -94,7 +94,7 @@ class ProfileViewModel @Inject constructor( profileRepository.updateProfile( fullName = fullName.value, country = countryCodeName.value ?: "", - email = if (currentEmail == email.value) null else email.value, + email = email.value, pfpFile.value ).collectLatest { resource -> if (resource is Resource.Success) { diff --git a/android/app/src/main/res/layout/ccp_auth.xml b/android/app/src/main/res/layout/ccp_auth.xml index 5434e9e4cc..97c03741be 100644 --- a/android/app/src/main/res/layout/ccp_auth.xml +++ b/android/app/src/main/res/layout/ccp_auth.xml @@ -7,13 +7,13 @@ android:paddingVertical="12dp" app:ccp_autoDetectLanguage="true" app:ccp_contentColor="@color/white_primary" - app:ccpDialog_backgroundColor="@color/transparent" + app:ccpDialog_backgroundColor="@color/black_secondary" app:ccpDialog_textColor="@color/white_primary" app:ccp_arrowColor="@color/white_primary" app:ccp_flagBorderColor="@color/white_primary" app:ccp_textGravity="LEFT" app:ccp_padding="0dp" - app:ccpDialog_background="@color/transparent" + app:ccpDialog_background="@color/black_secondary" app:ccpDialog_cornerRadius="16dp" app:ccp_showFullName="true" app:ccp_showPhoneCode="false"> diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index e9c650eef8..9859992657 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -2226,7 +2226,7 @@ В процессе удаления Пожалуйста подождите данные скачиваются Пусто - Отзыв будет публикован когда будете онлайн + Отзыв будет публикован когда будете онлайн Отзыв был успешно опубликован Не удалось публиковать отзыв Поажалуйста, не выходите за рамки Таджикистана, вы должны быть в Таджикистане diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index b114c85b06..bb248b7321 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -2268,7 +2268,7 @@ Deleting… Please, wait, data being downloaded Пусто - Review will be published when you are online + Review will be published when you are online Review was successfully published Failed to publish review\n Please, don\'t go out of Tajikistan, it\'s Tajikistan app diff --git a/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings b/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings index a98bbcd455..30c1acda3d 100644 --- a/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings +++ b/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings @@ -4053,7 +4053,7 @@ "retry" = "Try again"; -"no_network" = "Couldn't reach the server, please check connection"; +"no_connection" = "Couldn't reach the server, please check connection"; "no_image" = "No image"; @@ -4101,7 +4101,7 @@ "back" = "Back"; -"review_will_be_published" = "Review will be published when you are online"; +"review_will_be_published_when_online" = "Review will be published when you are online"; "review_was_published" = "Review was successfully published"; diff --git a/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings b/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings index 3efefc2d7b..d2416cc9c0 100644 --- a/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings +++ b/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings @@ -4053,7 +4053,7 @@ "retry" = "Try again"; -"no_network" = "Couldn't reach the server, please check connection"; +"no_connection" = "Couldn't reach the server, please check connection"; "no_image" = "No image"; @@ -4101,7 +4101,7 @@ "back" = "Back"; -"review_will_be_published" = "Review will be published when you are online"; +"review_will_be_published_when_online" = "Review will be published when you are online"; "review_was_published" = "Review was successfully published"; diff --git a/iphone/Maps/LocalizedStrings/ru.lproj/Localizable.strings b/iphone/Maps/LocalizedStrings/ru.lproj/Localizable.strings index dc377fc954..8c6e167841 100644 --- a/iphone/Maps/LocalizedStrings/ru.lproj/Localizable.strings +++ b/iphone/Maps/LocalizedStrings/ru.lproj/Localizable.strings @@ -4053,7 +4053,7 @@ "retry" = "Попробовать заново"; -"no_network" = "Не удается соединиться с сервером, проверьте интернет подключение"; +"no_connection" = "Не удается соединиться с сервером, проверьте интернет подключение"; "no_image" = "Нет фото"; @@ -4101,7 +4101,7 @@ "back" = "Назад"; -"review_will_be_published" = "Отзыв будет публикован когда будете онлайн"; +"review_will_be_published_when_online" = "Отзыв будет публикован когда будете онлайн"; "review_was_published" = "Отзыв был успешно опубликован"; diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index 3d7cd321fb..a36afc49a2 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -305,14 +305,11 @@ 529A5F192C85BFF0004FE4A1 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F182C85BFF0004FE4A1 /* ToastView.swift */; }; 529A5F1E2C86DDE5004FE4A1 /* PlaceDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F1D2C86DDE5004FE4A1 /* PlaceDTO.swift */; }; 529A5F202C86DE14004FE4A1 /* CoordinatesDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F1F2C86DE14004FE4A1 /* CoordinatesDTO.swift */; }; - 529A5F222C86DE50004FE4A1 /* ReviewDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F212C86DE50004FE4A1 /* ReviewDTO.swift */; }; - 529A5F242C86DE7D004FE4A1 /* ReviewIdsDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F232C86DE7D004FE4A1 /* ReviewIdsDTO.swift */; }; - 529A5F262C86DE9D004FE4A1 /* ReviewsDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F252C86DE9D004FE4A1 /* ReviewsDTO.swift */; }; + 529A5F222C86DE50004FE4A1 /* Reviews DTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F212C86DE50004FE4A1 /* Reviews DTOs.swift */; }; 529A5F282C86DEC5004FE4A1 /* UserDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F272C86DEC5004FE4A1 /* UserDTO.swift */; }; 529A5F2B2C86DF2D004FE4A1 /* PlaceShort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F2A2C86DF2D004FE4A1 /* PlaceShort.swift */; }; 529A5F2D2C86DF3B004FE4A1 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F2C2C86DF3B004FE4A1 /* User.swift */; }; - 529A5F2F2C86DF51004FE4A1 /* ReviewToPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F2E2C86DF51004FE4A1 /* ReviewToPost.swift */; }; - 529A5F312C86DF61004FE4A1 /* Review.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F302C86DF61004FE4A1 /* Review.swift */; }; + 529A5F312C86DF61004FE4A1 /* Review Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F302C86DF61004FE4A1 /* Review Models.swift */; }; 529A5F332C86DF6F004FE4A1 /* PlaceFull.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F322C86DF6F004FE4A1 /* PlaceFull.swift */; }; 529A5F352C86DF99004FE4A1 /* PlaceLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F342C86DF99004FE4A1 /* PlaceLocation.swift */; }; 529A5F372C86E02E004FE4A1 /* AllDataDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F362C86E02E004FE4A1 /* AllDataDTO.swift */; }; @@ -590,6 +587,8 @@ CE64501D2C93F8350075A59B /* ReviewsPersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE64501C2C93F8350075A59B /* ReviewsPersistenceController.swift */; }; CE6450202C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE64501F2C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift */; }; CE6450242C9772310075A59B /* DownloadProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6450232C9772310075A59B /* DownloadProgress.swift */; }; + CE6450282C99572F0075A59B /* ImageStoreUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6450272C99572F0075A59B /* ImageStoreUtils.swift */; }; + CEA45BC42C9AE01000ABE6B2 /* DataSyncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA45BC32C9AE01000ABE6B2 /* DataSyncer.swift */; }; CED0E00E2C8ACBCA008C61CA /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = CED0E00D2C8ACBCA008C61CA /* SDWebImageSwiftUI */; }; CED0E0112C8ACBE1008C61CA /* CountryPickerView in Frameworks */ = {isa = PBXBuildFile; productRef = CED0E0102C8ACBE1008C61CA /* CountryPickerView */; }; CED0E0172C8ACF0D008C61CA /* RoundedCornerShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0162C8ACF0D008C61CA /* RoundedCornerShape.swift */; }; @@ -1373,14 +1372,11 @@ 529A5F182C85BFF0004FE4A1 /* ToastView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; 529A5F1D2C86DDE5004FE4A1 /* PlaceDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceDTO.swift; sourceTree = ""; }; 529A5F1F2C86DE14004FE4A1 /* CoordinatesDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatesDTO.swift; sourceTree = ""; }; - 529A5F212C86DE50004FE4A1 /* ReviewDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewDTO.swift; sourceTree = ""; }; - 529A5F232C86DE7D004FE4A1 /* ReviewIdsDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewIdsDTO.swift; sourceTree = ""; }; - 529A5F252C86DE9D004FE4A1 /* ReviewsDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsDTO.swift; sourceTree = ""; }; + 529A5F212C86DE50004FE4A1 /* Reviews DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Reviews DTOs.swift"; sourceTree = ""; }; 529A5F272C86DEC5004FE4A1 /* UserDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDTO.swift; sourceTree = ""; }; 529A5F2A2C86DF2D004FE4A1 /* PlaceShort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceShort.swift; sourceTree = ""; }; 529A5F2C2C86DF3B004FE4A1 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; - 529A5F2E2C86DF51004FE4A1 /* ReviewToPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewToPost.swift; sourceTree = ""; }; - 529A5F302C86DF61004FE4A1 /* Review.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Review.swift; sourceTree = ""; }; + 529A5F302C86DF61004FE4A1 /* Review Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review Models.swift"; sourceTree = ""; }; 529A5F322C86DF6F004FE4A1 /* PlaceFull.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceFull.swift; sourceTree = ""; }; 529A5F342C86DF99004FE4A1 /* PlaceLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceLocation.swift; sourceTree = ""; }; 529A5F362C86E02E004FE4A1 /* AllDataDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDataDTO.swift; sourceTree = ""; }; @@ -1643,6 +1639,8 @@ CE64501C2C93F8350075A59B /* ReviewsPersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsPersistenceController.swift; sourceTree = ""; }; CE64501F2C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsPersistenceControllerTesterBro.swift; sourceTree = ""; }; CE6450232C9772310075A59B /* DownloadProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadProgress.swift; sourceTree = ""; }; + CE6450272C99572F0075A59B /* ImageStoreUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStoreUtils.swift; sourceTree = ""; }; + CEA45BC32C9AE01000ABE6B2 /* DataSyncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSyncer.swift; sourceTree = ""; }; CED0E0162C8ACF0D008C61CA /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = ""; }; CED0E0182C8AD57C008C61CA /* EmptyUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUI.swift; sourceTree = ""; }; CED0E01A2C8B048C008C61CA /* AllPicsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPicsScreen.swift; sourceTree = ""; }; @@ -3173,9 +3171,7 @@ isa = PBXGroup; children = ( 529A5F1D2C86DDE5004FE4A1 /* PlaceDTO.swift */, - 529A5F212C86DE50004FE4A1 /* ReviewDTO.swift */, - 529A5F232C86DE7D004FE4A1 /* ReviewIdsDTO.swift */, - 529A5F252C86DE9D004FE4A1 /* ReviewsDTO.swift */, + 529A5F212C86DE50004FE4A1 /* Reviews DTOs.swift */, 529A5F272C86DEC5004FE4A1 /* UserDTO.swift */, 529A5F1F2C86DE14004FE4A1 /* CoordinatesDTO.swift */, ); @@ -3264,6 +3260,7 @@ isa = PBXGroup; children = ( 527D5E7A2C60E05D00736A85 /* LanguageUtils.swift */, + CE6450272C99572F0075A59B /* ImageStoreUtils.swift */, ); path = Utils; sourceTree = ""; @@ -3302,8 +3299,7 @@ children = ( 529A5F2A2C86DF2D004FE4A1 /* PlaceShort.swift */, 529A5F2C2C86DF3B004FE4A1 /* User.swift */, - 529A5F2E2C86DF51004FE4A1 /* ReviewToPost.swift */, - 529A5F302C86DF61004FE4A1 /* Review.swift */, + 529A5F302C86DF61004FE4A1 /* Review Models.swift */, 529A5F322C86DF6F004FE4A1 /* PlaceFull.swift */, CED0E0442C918ED4008C61CA /* Hash.swift */, ); @@ -3405,6 +3401,7 @@ 52E2D39B2C58E72900A8843A /* Screens */, 524634CC2C57232400FDCABA /* TourismMain.storyboard */, 52522F3A2C6DDA750015709C /* ThemeViewModel.swift */, + CEA45BC32C9AE01000ABE6B2 /* DataSyncer.swift */, ); path = Home; sourceTree = ""; @@ -5040,6 +5037,7 @@ 471A7BB8247FE3C300A0D4C1 /* URL+Query.swift in Sources */, 47F86D0120C93D8D00FEE291 /* TabViewController.swift in Sources */, 52E95F022C6B32E500A3FE2E /* ErrorResponse.swift in Sources */, + CE6450282C99572F0075A59B /* ImageStoreUtils.swift in Sources */, 99536113235DB86C008B218F /* InsetsLabel.swift in Sources */, 52ECA8182C8A255900F213B3 /* PlaceTabsBar.swift in Sources */, 6741A9A51BF340DE002C974C /* MWMShareActivityItem.mm in Sources */, @@ -5208,7 +5206,6 @@ 34AB66051FC5AA320078E451 /* MWMNavigationDashboardManager+Entity.mm in Sources */, 993DF12A23F6BDB100AC231A /* Style.swift in Sources */, 34ABA6171C2D185C00FE1BEC /* MWMAuthorizationOSMLoginViewController.mm in Sources */, - 529A5F242C86DE7D004FE4A1 /* ReviewIdsDTO.swift in Sources */, ED9966802B94FBC20083CE55 /* ColorPicker.swift in Sources */, 993DF10423F6BDB100AC231A /* UIView+styleName.swift in Sources */, 998927302449DE1500260CE2 /* TabBarArea.swift in Sources */, @@ -5257,7 +5254,7 @@ 34C9BD031C6DB693000DC38D /* MWMTableViewController.m in Sources */, 52E95F0D2C6C797B00A3FE2E /* ProfileViewController.swift in Sources */, F6E2FD8C1E097BA00083EBEC /* MWMNoMapsView.m in Sources */, - 529A5F312C86DF61004FE4A1 /* Review.swift in Sources */, + 529A5F312C86DF61004FE4A1 /* Review Models.swift in Sources */, 34D3B0361E389D05004100F9 /* MWMEditorSelectTableViewCell.m in Sources */, 990128562449A82500C72B10 /* BottomTabBarView.swift in Sources */, 529A5F422C86E108004FE4A1 /* Category.swift in Sources */, @@ -5327,7 +5324,6 @@ CED0E0282C8C85C9008C61CA /* PostReviewView.swift in Sources */, 529A5F5E2C86E37A004FE4A1 /* PlacesItem.swift in Sources */, 340475591E081A4600C92850 /* WebViewController.m in Sources */, - 529A5F262C86DE9D004FE4A1 /* ReviewsDTO.swift in Sources */, 3404F4992028A20D0090E401 /* BMCCategoryCell.swift in Sources */, F62607FD207B790300176C5A /* SpinnerAlert.swift in Sources */, 3444DFD21F17620C00E73099 /* MWMMapWidgetsHelper.mm in Sources */, @@ -5362,7 +5358,7 @@ F660DEE51EAF4F59004DC056 /* MWMLocationManager+SpeedAndAltitude.swift in Sources */, F6E2FDF21E097BA00083EBEC /* MWMOpeningHoursAddScheduleTableViewCell.mm in Sources */, 3304306D21D4EAFB00317CA3 /* SearchCategoryCell.swift in Sources */, - 529A5F222C86DE50004FE4A1 /* ReviewDTO.swift in Sources */, + 529A5F222C86DE50004FE4A1 /* Reviews DTOs.swift in Sources */, ED79A5AB2BD7AA9C00952D1F /* LoadingOverlayViewController.swift in Sources */, 34AB66111FC5AA320078E451 /* NavigationTurnsView.swift in Sources */, 475ED78624C7C7300063ADC7 /* ValueStepperViewRenderer.swift in Sources */, @@ -5387,7 +5383,6 @@ 477219052243E79500E5B227 /* DrivingOptionsViewController.swift in Sources */, CDB4D4E4222E8FF600104869 /* CarPlayService.swift in Sources */, F6E2FF3C1E097BA00083EBEC /* MWMSearchTableView.m in Sources */, - 529A5F2F2C86DF51004FE4A1 /* ReviewToPost.swift in Sources */, 52ED91A72C72C58A000EE25B /* CurrencyPersistenceController.swift in Sources */, F6E2FF661E097BA00083EBEC /* MWMTTSSettingsViewController.mm in Sources */, 3454D7C21E07F045004AF2AD /* NSString+Categories.m in Sources */, @@ -5483,6 +5478,7 @@ F6E2FE221E097BA00083EBEC /* MWMOpeningHoursEditorViewController.mm in Sources */, ED79A5D72BDF8D6100952D1F /* SynchronizationStateManager.swift in Sources */, 999FC12B23ABB4B800B0E6F9 /* FontStyleSheet.swift in Sources */, + CEA45BC42C9AE01000ABE6B2 /* DataSyncer.swift in Sources */, 47CA68DA2500469400671019 /* BookmarksListBuilder.swift in Sources */, 34D3AFE21E376F7E004100F9 /* UITableView+Updates.swift in Sources */, 3404164C1E7BF42E00E2B6D6 /* UIView+Coordinates.swift in Sources */, diff --git a/iphone/Maps/Tourism/Data/Db/DataModels/Place.xcdatamodeld/Place.xcdatamodel/contents b/iphone/Maps/Tourism/Data/Db/DataModels/Place.xcdatamodeld/Place.xcdatamodel/contents index c75601dc8f..15d05edb6b 100644 --- a/iphone/Maps/Tourism/Data/Db/DataModels/Place.xcdatamodeld/Place.xcdatamodel/contents +++ b/iphone/Maps/Tourism/Data/Db/DataModels/Place.xcdatamodeld/Place.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -32,9 +32,8 @@ - - + \ No newline at end of file diff --git a/iphone/Maps/Tourism/Data/Db/DataModels/UserEntity.swift b/iphone/Maps/Tourism/Data/Db/DataModels/UserEntity.swift index 4e479d5393..05f01b715c 100644 --- a/iphone/Maps/Tourism/Data/Db/DataModels/UserEntity.swift +++ b/iphone/Maps/Tourism/Data/Db/DataModels/UserEntity.swift @@ -1,4 +1,4 @@ -struct UserEntity: Encodable { +struct UserEntity: Codable { let userId: Int64 let fullName: String let avatar: String diff --git a/iphone/Maps/Tourism/Data/Db/EntitiesMapping.swift b/iphone/Maps/Tourism/Data/Db/EntitiesMapping.swift index 38bfe102fb..6b69edd2db 100644 --- a/iphone/Maps/Tourism/Data/Db/EntitiesMapping.swift +++ b/iphone/Maps/Tourism/Data/Db/EntitiesMapping.swift @@ -81,3 +81,38 @@ extension UserEntity { ) } } + +extension ReviewEntity { + func toReview() -> Review { + + let user = DBUtils.decodeFromJsonString(self.userJson ?? "", to: UserEntity.self)?.toUser() + let picsUrls = DBUtils.decodeFromJsonString(self.picsUrlsJson ?? "", to: [String].self) + + return Review( + id: self.id, + placeId: self.placeId, + rating: Int(self.rating), + user: user, + date: self.date, + comment: self.comment, + picsUrls: picsUrls ?? [], + deletionPlanned: self.deletionPlanned + ) + } +} + +extension ReviewPlannedToPostEntity { + func toReviewToPostDTO() -> ReviewToPostDTO { + var images = [String]() + if let imagesJson = self.imagesJson { + images = DBUtils.decodeFromJsonString(imagesJson, to: [String].self) ?? [] + } + + return ReviewToPostDTO( + placeId: self.placeId, + comment: self.comment ?? "", + rating: Int(self.rating), + images: images.map { URL(string: $0)! } + ) + } +} diff --git a/iphone/Maps/Tourism/Data/Db/PersistenceControllers/PlacesPersistenceController.swift b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/PlacesPersistenceController.swift index f3324da2ee..6204531606 100644 --- a/iphone/Maps/Tourism/Data/Db/PersistenceControllers/PlacesPersistenceController.swift +++ b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/PlacesPersistenceController.swift @@ -300,7 +300,7 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate } } - func addFavoriteSync(placeId: Int64, isFavorite: Bool) { + func addFavoritingRecordForSync(placeId: Int64, isFavorite: Bool) { let context = container.viewContext let favoriteSyncEntity = FavoriteSyncEntity(context: context) favoriteSyncEntity.placeId = placeId @@ -313,23 +313,28 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate } } - func removeFavoriteSync(placeIds: [Int64]) { + func removeFavoritingRecordsForSync(placeIds: [Int64]) { let context = container.viewContext - let fetchRequest: NSFetchRequest = FavoriteSyncEntity.fetchRequest() + let fetchRequest: NSFetchRequest = FavoriteSyncEntity.fetchRequest() fetchRequest.predicate = NSPredicate(format: "placeId IN %@", placeIds) + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + deleteRequest.resultType = .resultTypeObjectIDs + do { - let favoriteSyncs = try context.fetch(fetchRequest) - for favoriteSync in favoriteSyncs { - context.delete(favoriteSync) - } + let result = try context.execute(deleteRequest) as? NSBatchDeleteResult + let changes: [AnyHashable: Any] = [ + NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? [] + ] + + NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context]) try context.save() } catch { - print("Failed to remove favorite syncs: \(error)") + print("Failed to remove favoriting records for sync: \(error)") } } - func getFavoriteSyncData() -> [FavoriteSyncEntity] { + func getFavoritingRecordsForSync() -> [FavoriteSyncEntity] { let context = container.viewContext let fetchRequest: NSFetchRequest = FavoriteSyncEntity.fetchRequest() diff --git a/iphone/Maps/Tourism/Data/Db/PersistenceControllers/ReviewsPersistenceController.swift b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/ReviewsPersistenceController.swift index 962b8c7c1a..1f36d9c674 100644 --- a/iphone/Maps/Tourism/Data/Db/PersistenceControllers/ReviewsPersistenceController.swift +++ b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/ReviewsPersistenceController.swift @@ -31,7 +31,6 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate do { let results = try context.fetch(fetchRequest) - let reviewEntity: ReviewEntity if let existingReview = results.first { // Update existing review updateReviewEntity(existingReview, with: review) @@ -55,7 +54,6 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate for review in reviews { fetchRequest.predicate = NSPredicate(format: "id == %lld", review.id) let results = try context.fetch(fetchRequest) - let reviewEntity: ReviewEntity if let existingReview = results.first { // Update existing review updateReviewEntity(existingReview, with: review) @@ -75,7 +73,7 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate private func updateReviewEntity(_ entity: ReviewEntity, with review: Review) { entity.placeId = review.placeId entity.rating = Int16(review.rating) - entity.userJson = DBUtils.encodeToJsonString(review.user.toUserEntity()) + entity.userJson = DBUtils.encodeToJsonString(review.user?.toUserEntity()) entity.date = review.date entity.comment = review.comment entity.picsUrlsJson = DBUtils.encodeToJsonString(review.picsUrls) @@ -120,8 +118,6 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate let context = container.viewContext let fetchRequest: NSFetchRequest = ReviewEntity.fetchRequest() let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) - - // Configure the request to return the IDs of the deleted objects deleteRequest.resultType = .resultTypeObjectIDs do { @@ -140,14 +136,18 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate func deleteAllPlaceReviews(placeId: Int64) { let context = container.viewContext - let fetchRequest: NSFetchRequest = ReviewEntity.fetchRequest() + let fetchRequest: NSFetchRequest = ReviewEntity.fetchRequest() fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId) + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + deleteRequest.resultType = .resultTypeObjectIDs do { - let reviews = try context.fetch(fetchRequest) - for review in reviews { - context.delete(review) - } + let result = try context.execute(deleteRequest) as? NSBatchDeleteResult + let changes: [AnyHashable: Any] = [ + NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? [] + ] + + NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context]) try context.save() } catch { print(error) @@ -158,7 +158,7 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate func observeReviewsForPlace(placeId: Int64) { let fetchRequest: NSFetchRequest = ReviewEntity.fetchRequest() fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId) - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)] + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)] reviewsForPlaceFetchedResultsController = NSFetchedResultsController( fetchRequest: fetchRequest, @@ -172,7 +172,9 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate do { try reviewsForPlaceFetchedResultsController?.performFetch() if let results = reviewsForPlaceFetchedResultsController?.fetchedObjects { - reviewsForPlaceSubject.send(results) + reviewsForPlaceSubject.send(results.map({ reviews in + reviews.toReview() + })) } } catch { print(error) @@ -213,10 +215,14 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate // // MARK: - Planned Review Operations - func insertReviewPlannedToPost(_ review: ReviewPlannedToPostEntity) { + func insertReviewPlannedToPost(_ review: ReviewToPost) { let context = container.viewContext let newReview = ReviewPlannedToPostEntity(context: context) - // Set properties of newReview based on the input review + newReview.placeId = review.placeId + newReview.comment = review.comment + newReview.rating = Int32(review.rating) + let imagesJson = DBUtils.encodeToJsonString(review.images) + newReview.imagesJson = imagesJson do { try context.save() @@ -228,14 +234,19 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate func deleteReviewPlannedToPost(placeId: Int64) { let context = container.viewContext - let fetchRequest: NSFetchRequest = ReviewPlannedToPostEntity.fetchRequest() + let fetchRequest: NSFetchRequest = ReviewPlannedToPostEntity.fetchRequest() fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId) + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + deleteRequest.resultType = .resultTypeObjectIDs + do { - let reviews = try context.fetch(fetchRequest) - for review in reviews { - context.delete(review) - } + let result = try context.execute(deleteRequest) as? NSBatchDeleteResult + let changes: [AnyHashable: Any] = [ + NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? [] + ] + + NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context]) try context.save() } catch { print(error) @@ -256,29 +267,32 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate } } - func observeReviewsPlannedToPost() { - let fetchRequest: NSFetchRequest = ReviewPlannedToPostEntity.fetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "placeId", ascending: true)] - - reviewsPlannedToPostFetchedResultsController = NSFetchedResultsController( - fetchRequest: fetchRequest, - managedObjectContext: container.viewContext, - sectionNameKeyPath: nil, - cacheName: nil - ) - - reviewsPlannedToPostFetchedResultsController?.delegate = self - - do { - try reviewsPlannedToPostFetchedResultsController?.performFetch() - if let results = reviewsPlannedToPostFetchedResultsController?.fetchedObjects { - reviewsPlannedToPostSubject.send(results) - } - } catch { - print(error) - reviewsPlannedToPostSubject.send(completion: .failure(ResourceError.cacheError)) - } - } + // we only use it to limit the user from reviewing when he already made review + // for a place +// func observeReviewsPlannedToPost(placeId: Int64) { +// let fetchRequest: NSFetchRequest = ReviewPlannedToPostEntity.fetchRequest() +// fetchRequest.sortDescriptors = [NSSortDescriptor(key: "placeId", ascending: true)] +// fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId) +// +// reviewsPlannedToPostFetchedResultsController = NSFetchedResultsController( +// fetchRequest: fetchRequest, +// managedObjectContext: container.viewContext, +// sectionNameKeyPath: nil, +// cacheName: nil +// ) +// +// reviewsPlannedToPostFetchedResultsController?.delegate = self +// +// do { +// try reviewsPlannedToPostFetchedResultsController?.performFetch() +// if let results = reviewsPlannedToPostFetchedResultsController?.fetchedObjects { +// reviewsPlannedToPostSubject.send(results) +// } +// } catch { +// print(error) +// reviewsPlannedToPostSubject.send(completion: .failure(ResourceError.cacheError)) +// } +// } // MARK: - NSFetchedResultsControllerDelegate func controllerDidChangeContent(_ controller: NSFetchedResultsController) { @@ -288,7 +302,9 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate switch controller { case reviewsForPlaceFetchedResultsController: - reviewsForPlaceSubject.send(fetchedObjects as! [Review]) + let reviewsEntities = fetchedObjects as! [ReviewEntity] + let reviews = reviewsEntities.map { reviewEntity in reviewEntity.toReview() } + reviewsForPlaceSubject.send(reviews) case reviewsPlannedToPostFetchedResultsController: reviewsPlannedToPostSubject.send(fetchedObjects as! [ReviewPlannedToPostEntity]) default: diff --git a/iphone/Maps/Tourism/Data/Network/DTO/Details/ReviewIdsDTO.swift b/iphone/Maps/Tourism/Data/Network/DTO/Details/ReviewIdsDTO.swift deleted file mode 100644 index 29678fb69a..0000000000 --- a/iphone/Maps/Tourism/Data/Network/DTO/Details/ReviewIdsDTO.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -struct ReviewIdsDTO: Codable { - let feedbacks: [Int64] -} diff --git a/iphone/Maps/Tourism/Data/Network/DTO/Details/ReviewDTO.swift b/iphone/Maps/Tourism/Data/Network/DTO/Details/Reviews DTOs.swift similarity index 63% rename from iphone/Maps/Tourism/Data/Network/DTO/Details/ReviewDTO.swift rename to iphone/Maps/Tourism/Data/Network/DTO/Details/Reviews DTOs.swift index aad1917716..04c2100e94 100644 --- a/iphone/Maps/Tourism/Data/Network/DTO/Details/ReviewDTO.swift +++ b/iphone/Maps/Tourism/Data/Network/DTO/Details/Reviews DTOs.swift @@ -21,3 +21,21 @@ struct ReviewDTO: Codable { ) } } + + +struct ReviewIdsDTO: Codable { + let feedbacks: [Int64] +} + + +struct ReviewsDTO: Codable { + let data: [ReviewDTO] +} + + +struct ReviewToPostDTO: Codable { + let placeId: Int64 + let comment: String + let rating: Int + let images: [URL] +} diff --git a/iphone/Maps/Tourism/Data/Network/DTO/Details/ReviewsDTO.swift b/iphone/Maps/Tourism/Data/Network/DTO/Details/ReviewsDTO.swift deleted file mode 100644 index fa35a6ca8f..0000000000 --- a/iphone/Maps/Tourism/Data/Network/DTO/Details/ReviewsDTO.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -struct ReviewsDTO: Codable { - let data: [ReviewDTO] -} diff --git a/iphone/Maps/Tourism/Data/Network/Services/ReviewsService.swift b/iphone/Maps/Tourism/Data/Network/Services/ReviewsService.swift index 985df9a93a..8b92e2d112 100644 --- a/iphone/Maps/Tourism/Data/Network/Services/ReviewsService.swift +++ b/iphone/Maps/Tourism/Data/Network/Services/ReviewsService.swift @@ -2,20 +2,91 @@ import Combine protocol ReviewsService { func getReviewsByPlaceId(id: Int64) async throws -> ReviewsDTO - func postReview(review: ReviewToPost) async throws -> ReviewDTO - func deleteReview(feedbacks: ReviewIdsDTO) async throws -> SimpleResponse + func postReview(review: ReviewToPostDTO) async throws -> SimpleResponse + func deleteReview(reviews: ReviewIdsDTO) async throws -> SimpleResponse } class ReviewsServiceImpl : ReviewsService { + let userPreferences: UserPreferences + + init(userPreferences: UserPreferences) { + self.userPreferences = userPreferences + } + func getReviewsByPlaceId(id: Int64) async throws -> ReviewsDTO { return try await AppNetworkHelper.get(path: APIEndpoints.getReviewsByPlaceIdUrl(id: id)) } - func postReview(review: ReviewToPost) async throws -> ReviewDTO { - return try await AppNetworkHelper.post(path: APIEndpoints.postReviewUrl, body: review) + func postReview(review: ReviewToPostDTO) async throws -> SimpleResponse { + guard let url = URL(string: APIEndpoints.postReviewUrl) else { + throw ResourceError.other(message: "Invalid URL") + } + + let boundary = "Boundary-\(UUID().uuidString)" + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Accept") + if let token = userPreferences.getToken() { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + let parameters: [[String: Any]] = [ + ["key": "message", "value": review.comment, "type": "text"], + ["key": "mark_id", "value": "\(review.placeId)", "type": "text"], + ["key": "points", "value": "\(review.rating)", "type": "text"] + ] + review.images.map { ["key": "images[]", "src": $0.path, "type": "file"] } + + let body = try createBody(with: parameters, boundary: boundary) + request.httpBody = body + + let (data, response) = try await URLSession.shared.data(for: request) + + if let httpResponse = response as? HTTPURLResponse { + if !(200...299).contains(httpResponse.statusCode) { + throw ResourceError.other(message: "Response not successful") + } + } + + let decoder = JSONDecoder() + return try decoder.decode(SimpleResponse.self, from: data) } - func deleteReview(feedbacks: ReviewIdsDTO) async throws -> SimpleResponse { - return try await AppNetworkHelper.delete(path: APIEndpoints.deleteReviewsUrl, body: feedbacks) + private func createBody(with parameters: [[String: Any]], boundary: String) throws -> Data { + var body = Data() + + for param in parameters { + if param["disabled"] != nil { continue } + + let paramName = param["key"] as! String + body.append("--\(boundary)\r\n".data(using: .utf8)!) + body.append("Content-Disposition:form-data; name=\"\(paramName)\"".data(using: .utf8)!) + + if let contentType = param["contentType"] as? String { + body.append("\r\nContent-Type: \(contentType)".data(using: .utf8)!) + } + + let paramType = param["type"] as! String + if paramType == "text" { + let paramValue = param["value"] as! String + body.append("\r\n\r\n\(paramValue)\r\n".data(using: .utf8)!) + } else { + let paramSrc = param["src"] as! String + let fileURL = URL(fileURLWithPath: paramSrc) + let fileName = fileURL.lastPathComponent + let data = try Data(contentsOf: fileURL) + body.append("; filename=\"\(fileName)\"\r\n".data(using: .utf8)!) + body.append("Content-Type: \"content-type header\"\r\n\r\n".data(using: .utf8)!) + body.append(data) + body.append("\r\n".data(using: .utf8)!) + } + } + + body.append("--\(boundary)--\r\n".data(using: .utf8)!) + return body + } + + func deleteReview(reviews: ReviewIdsDTO) async throws -> SimpleResponse { + return try await AppNetworkHelper.delete(path: APIEndpoints.deleteReviewsUrl, body: reviews) } } diff --git a/iphone/Maps/Tourism/Data/Network/Utils/NetworkHelper.swift b/iphone/Maps/Tourism/Data/Network/Utils/NetworkHelper.swift index fe29f4c7e8..cffd6e576c 100644 --- a/iphone/Maps/Tourism/Data/Network/Utils/NetworkHelper.swift +++ b/iphone/Maps/Tourism/Data/Network/Utils/NetworkHelper.swift @@ -163,7 +163,7 @@ class AppNetworkHelper { headers: headers, decoder: decoder ) - } catch { + } catch let error as NSError { print(error) throw ResourceError.other(message: "Encoding error") } diff --git a/iphone/Maps/Tourism/Data/Repositories/PlacesRepositoryImpl.swift b/iphone/Maps/Tourism/Data/Repositories/PlacesRepositoryImpl.swift index 279d6f4ad5..c115b7733c 100644 --- a/iphone/Maps/Tourism/Data/Repositories/PlacesRepositoryImpl.swift +++ b/iphone/Maps/Tourism/Data/Repositories/PlacesRepositoryImpl.swift @@ -181,7 +181,7 @@ class PlacesRepositoryImpl: PlacesRepository { func setFavorite(placeId: Int64, isFavorite: Bool) { placesPersistenceController.setFavorite(placeId: placeId, isFavorite: isFavorite) - placesPersistenceController.addFavoriteSync( + placesPersistenceController.addFavoritingRecordForSync( placeId: placeId, isFavorite: isFavorite ) @@ -196,14 +196,42 @@ class PlacesRepositoryImpl: PlacesRepository { try await placesService.removeFromFavorites(ids: favoritesIdsDto) } - placesPersistenceController.removeFavoriteSync(placeIds: [placeId]) + placesPersistenceController.removeFavoritingRecordsForSync(placeIds: [placeId]) } catch { - placesPersistenceController.addFavoriteSync(placeId: placeId, isFavorite: isFavorite) + print("Failed to setFavorite") + print(error) } } } func syncFavorites() { - // TODO: cmon + let syncData = placesPersistenceController.getFavoritingRecordsForSync() + + let favoritesToAdd = syncData.filter { $0.isFavorite }.map { $0.placeId } + let favoritesToRemove = syncData.filter { !$0.isFavorite }.map { $0.placeId } + + if !favoritesToAdd.isEmpty { + Task { + do { + let response = + try await placesService.addFavorites(ids: FavoritesIdsDTO(marks: favoritesToAdd)) + placesPersistenceController.removeFavoritingRecordsForSync(placeIds: favoritesToAdd) + } catch { + print(error) + } + } + } + + if !favoritesToRemove.isEmpty { + Task { + do { + let response = + try await placesService.removeFromFavorites(ids: FavoritesIdsDTO(marks: favoritesToRemove)) + placesPersistenceController.removeFavoritingRecordsForSync(placeIds: favoritesToRemove) + } catch { + print(error) + } + } + } } } diff --git a/iphone/Maps/Tourism/Data/Repositories/ProfileRepositoryImpl.swift b/iphone/Maps/Tourism/Data/Repositories/ProfileRepositoryImpl.swift index b7c11f3a71..52112cc2fe 100644 --- a/iphone/Maps/Tourism/Data/Repositories/ProfileRepositoryImpl.swift +++ b/iphone/Maps/Tourism/Data/Repositories/ProfileRepositoryImpl.swift @@ -27,7 +27,7 @@ class ProfileRepositoryImpl: ProfileRepository { } receiveValue: { personalData in self.personalDataPassThroughSubject.send(personalData) } - .store(in: &cancellables) // Store the cancellable + .store(in: &cancellables) persistenceController.observePersonalData() @@ -40,6 +40,9 @@ class ProfileRepositoryImpl: ProfileRepository { } let newPersonalData = remotePersonalData.toPersonalData() + + userPreferences.setUserId(value: String(newPersonalData.id)) + return self.persistenceController.updatePersonalData(personalData: newPersonalData) .map { newPersonalData } .eraseToAnyPublisher() @@ -52,7 +55,7 @@ class ProfileRepositoryImpl: ProfileRepository { } receiveValue: { personalData in // Yes, nothing, we observe anyway } - .store(in: &cancellables) // Store the cancellable + .store(in: &cancellables) } func updateProfile( diff --git a/iphone/Maps/Tourism/Data/Repositories/ReviewsRepositoryImpl.swift b/iphone/Maps/Tourism/Data/Repositories/ReviewsRepositoryImpl.swift index ffb516c217..e4dbdbe55b 100644 --- a/iphone/Maps/Tourism/Data/Repositories/ReviewsRepositoryImpl.swift +++ b/iphone/Maps/Tourism/Data/Repositories/ReviewsRepositoryImpl.swift @@ -1,6 +1,8 @@ import Combine class ReviewsRepositoryImpl : ReviewsRepository { + private var cancellables = Set() + var reviewsPersistenceController: ReviewsPersistenceController var reviewsService: ReviewsService @@ -9,16 +11,16 @@ class ReviewsRepositoryImpl : ReviewsRepository { init( reviewsPersistenceController: ReviewsPersistenceController, - reviewsService: ReviewsService, - reviewsResource: PassthroughSubject<[Review], ResourceError> + reviewsService: ReviewsService ) { self.reviewsPersistenceController = reviewsPersistenceController self.reviewsService = reviewsService self.reviewsResource = reviewsPersistenceController.reviewsForPlaceSubject - reviewsPersistenceController.reviewsPlannedToPostSubject.sink { completion in } receiveValue: { reviews in - self.isThereReviewPlannedToPublishResource.send(reviews.isEmpty) + reviewsPersistenceController.reviewsPlannedToPostSubject.sink { _ in } receiveValue: { + reviews in self.isThereReviewPlannedToPublishResource.send(reviews.isEmpty) } + .store(in: &cancellables) } func observeReviewsForPlace(id: Int64) { @@ -28,27 +30,99 @@ class ReviewsRepositoryImpl : ReviewsRepository { let reviewsDTO = try await reviewsService.getReviewsByPlaceId(id: id) let reviews = reviewsDTO.data.map { reviewDto in reviewDto.toReview() } - reviewsPersistenceController.deleteAllPlaceReviews(placeId: id) - reviewsPersistenceController.putReviews(reviews) + self.reviewsPersistenceController.deleteAllPlaceReviews(placeId: id) + self.reviewsPersistenceController.putReviews(reviews) } } - - func isThereReviewPlannedToPublish(for placeId: Int64) { - reviewsPersistenceController.getReviewsPlannedToPost() + func checkIfThereIsReviewPlannedToPublish(for placeId: Int64) { + reviewsPersistenceController.observeReviewsForPlace(placeId: placeId) } func postReview(review: ReviewToPost) -> AnyPublisher { - // TODO: cmon - return PassthroughSubject().eraseToAnyPublisher() + return Future { promise in + Task { + if Reachability.isConnectedToNetwork() { + do { + let response = try await self.reviewsService.postReview(review: review.toReviewToPostDTO()) + self.updateReviewsForDb(id: review.placeId) + promise(.success(SimpleResponse(message: response.message))) + } catch let error as ResourceError { + print(error) + promise(.failure(error)) + } + } else { + // images files already were saved in viewmodel, so no need to save them here + self.reviewsPersistenceController.insertReviewPlannedToPost(review) + promise(.failure(ResourceError.errorToUser(message: L("review_will_be_published_when_online")))) + } + } + } + .eraseToAnyPublisher() } func deleteReview(id: Int64) -> AnyPublisher { - // TODO: cmon - return PassthroughSubject().eraseToAnyPublisher() + return Future { promise in + Task { + do { + let response = try await self.reviewsService.deleteReview(reviews: ReviewIdsDTO(feedbacks: [id])) + self.reviewsPersistenceController.deleteReview(id: id) + promise(.success(response)) + } catch let error as ResourceError { + self.reviewsPersistenceController.markReviewForDeletion(id: id, deletionPlanned: true) + promise(.failure(error)) + } + } + } + .eraseToAnyPublisher() } func syncReviews() { - // TODO: cmon + Task { + try await deleteReviewsPlannedForDeletion() + try await publishReviewsPlannedToPost() + } + } + + private func deleteReviewsPlannedForDeletion() async throws { + let reviews = reviewsPersistenceController.getReviewsPlannedForDeletion() + + if !reviews.isEmpty { + let reviewsIds = reviews.map(\.id) + let response = try await reviewsService.deleteReview(reviews: ReviewIdsDTO(feedbacks: reviewsIds)) + reviewsPersistenceController.deleteReviews(ids: reviewsIds) + } + } + + private func publishReviewsPlannedToPost() async throws { + let reviewsPlannedToPostEntities = reviewsPersistenceController.getReviewsPlannedToPost() + if !reviewsPlannedToPostEntities.isEmpty { + let reviewsDTO = reviewsPlannedToPostEntities.map {$0.toReviewToPostDTO()} + reviewsDTO.forEach { reviewDTO in + Task { + do { + let response = try await reviewsService.postReview(review: reviewDTO) + updateReviewsForDb(id: reviewDTO.placeId) + reviewsPersistenceController.deleteReviewPlannedToPost(placeId: reviewDTO.placeId) + try reviewDTO.images.forEach { URL in + try FileManager.default.removeItem(at: URL) + } + } catch { + print(error) + } + } + } + } + } + + private func updateReviewsForDb(id: Int64) { + Task { + let reviewsDTO = try await reviewsService.getReviewsByPlaceId(id: id) + if !reviewsDTO.data.isEmpty { + reviewsPersistenceController.deleteAllReviews() + let reviews = reviewsDTO.data.map{ $0.toReview() } + reviewsPersistenceController.putReviews(reviews) + } + } } } diff --git a/iphone/Maps/Tourism/Data/ResourceError.swift b/iphone/Maps/Tourism/Data/ResourceError.swift index 39a0b1e826..060d2df69a 100644 --- a/iphone/Maps/Tourism/Data/ResourceError.swift +++ b/iphone/Maps/Tourism/Data/ResourceError.swift @@ -1,6 +1,7 @@ import Foundation enum ResourceError: Error, Equatable { + case noConnection case serverError(message: String) case cacheError case unauthed @@ -9,6 +10,8 @@ enum ResourceError: Error, Equatable { var errorDescription: String { switch self { + case .noConnection: + return L("no_connection") case .serverError: return L("server_error") case .cacheError: diff --git a/iphone/Maps/Tourism/Domain/Models/Details/Review Models.swift b/iphone/Maps/Tourism/Domain/Models/Details/Review Models.swift new file mode 100644 index 0000000000..88b955b476 --- /dev/null +++ b/iphone/Maps/Tourism/Domain/Models/Details/Review Models.swift @@ -0,0 +1,24 @@ +import Foundation + +struct Review: Codable, Hashable { + let id: Int64 + let placeId: Int64 + let rating: Int + let user: User? + let date: String? + let comment: String? + let picsUrls: [String] + var deletionPlanned: Bool = false +} + + +struct ReviewToPost: Codable { + let placeId: Int64 + let comment: String + let rating: Int + let images: [URL] + + func toReviewToPostDTO() -> ReviewToPostDTO { + return ReviewToPostDTO(placeId: placeId, comment: comment, rating: rating, images: images) + } +} diff --git a/iphone/Maps/Tourism/Domain/Models/Details/Review.swift b/iphone/Maps/Tourism/Domain/Models/Details/Review.swift deleted file mode 100644 index 152632dc28..0000000000 --- a/iphone/Maps/Tourism/Domain/Models/Details/Review.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation - -struct Review: Codable, Hashable { - let id: Int64 - let placeId: Int64 - let rating: Int - let user: User - let date: String? - let comment: String? - let picsUrls: [String] - var deletionPlanned: Bool = false -} diff --git a/iphone/Maps/Tourism/Domain/Models/Details/ReviewToPost.swift b/iphone/Maps/Tourism/Domain/Models/Details/ReviewToPost.swift deleted file mode 100644 index fa264636fd..0000000000 --- a/iphone/Maps/Tourism/Domain/Models/Details/ReviewToPost.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -struct ReviewToPost: Codable { - let placeId: Int64 - let comment: String - let rating: Int - let images: [URL] // Using URL to represent file paths -} diff --git a/iphone/Maps/Tourism/Domain/Repositories/ReviewsRepository.swift b/iphone/Maps/Tourism/Domain/Repositories/ReviewsRepository.swift index f5d0e0ba3b..45545ec1a9 100644 --- a/iphone/Maps/Tourism/Domain/Repositories/ReviewsRepository.swift +++ b/iphone/Maps/Tourism/Domain/Repositories/ReviewsRepository.swift @@ -5,7 +5,7 @@ protocol ReviewsRepository { func observeReviewsForPlace(id: Int64) var isThereReviewPlannedToPublishResource: PassthroughSubject { get } - func isThereReviewPlannedToPublish(for placeId: Int64) + func checkIfThereIsReviewPlannedToPublish(for placeId: Int64) func postReview(review: ReviewToPost) -> AnyPublisher diff --git a/iphone/Maps/Tourism/Presentation/Auth/Screens/WelcomeViewController.swift b/iphone/Maps/Tourism/Presentation/Auth/Screens/WelcomeViewController.swift index 4d30267329..0cd2b8579f 100644 --- a/iphone/Maps/Tourism/Presentation/Auth/Screens/WelcomeViewController.swift +++ b/iphone/Maps/Tourism/Presentation/Auth/Screens/WelcomeViewController.swift @@ -63,7 +63,7 @@ class WelcomeViewController: UIViewController { let label = UILabel() label.text = "©" label.textColor = .white - UIKitFont.applyStyle(to: label, style: UIKitFont.h1) + UIKitFont.applyStyle(to: label, style: UIKitFont.h2) label.translatesAutoresizingMaskIntoConstraints = false return label }() diff --git a/iphone/Maps/Tourism/Presentation/Components/LoadImg.swift b/iphone/Maps/Tourism/Presentation/Components/LoadImg.swift index 8faa640a25..42c9a621bc 100644 --- a/iphone/Maps/Tourism/Presentation/Components/LoadImg.swift +++ b/iphone/Maps/Tourism/Presentation/Components/LoadImg.swift @@ -4,16 +4,33 @@ import SDWebImageSwiftUI struct LoadImageView: View { let url: String? + @State var isError = false + var body: some View { if let urlString = url { - WebImage(url: URL(string: urlString)) - .resizable() - .indicator(.activity) - .scaledToFill() - .transition(.fade(duration: 0.2)) + ZStack(alignment: .center) { + WebImage(url: URL(string: urlString)) + .onSuccess(perform: { Image, data, cache in + self.isError = false + }) + .onFailure(perform: { isError in + self.isError = true + }) + .resizable() + .indicator(.activity) + .scaledToFill() + .transition(.fade(duration: 0.2)) + if(isError) { + Image(systemName: "exclamationmark.circle") + .font(.system(size: 30)) + .background(SwiftUI.Color.clear) + .foregroundColor(Color.hint) + .clipShape(Circle()) + } + } } else { Text(L("no_image")) - .foregroundColor(Color.surface) + .foregroundColor(Color.hint) } } } diff --git a/iphone/Maps/Tourism/Presentation/Home/DataSyncer.swift b/iphone/Maps/Tourism/Presentation/Home/DataSyncer.swift new file mode 100644 index 0000000000..f564088080 --- /dev/null +++ b/iphone/Maps/Tourism/Presentation/Home/DataSyncer.swift @@ -0,0 +1,80 @@ +import Network +import SystemConfiguration + +class DataSyncer { + private let reviewsRepository: ReviewsRepository + private let placesRepository: PlacesRepository + + init(reviewsRepository: ReviewsRepository, placesRepository: PlacesRepository) { + self.reviewsRepository = reviewsRepository + self.placesRepository = placesRepository + } + + private let monitor = NWPathMonitor() + private let queue = DispatchQueue.global(qos: .background) + + var isConnected: Bool = false + var isExpensive: Bool = false + + func startMonitoring() { + monitor.pathUpdateHandler = { path in + self.isConnected = path.status == .satisfied + self.isExpensive = path.isExpensive + + if path.status == .satisfied { + print("Connected to the internet.") + self.reviewsRepository.syncReviews() + self.placesRepository.syncFavorites() + } else { + print("No internet connection.") + } + + if path.isExpensive { + print("Connection is on an expensive network, like cellular.") + } + } + + monitor.start(queue: queue) + } + + func stopMonitoring() { + monitor.cancel() + } +} + + +public class Reachability { + + class func isConnectedToNetwork() -> Bool { + + var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)) + zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress)) + zeroAddress.sin_family = sa_family_t(AF_INET) + + let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in + SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress) + } + } + + var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0) + if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false { + return false + } + + /* Only Working for WIFI + let isReachable = flags == .reachable + let needsConnection = flags == .connectionRequired + + return isReachable && !needsConnection + */ + + // Working for Cellular and WIFI + let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0 + let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0 + let ret = (isReachable && !needsConnection) + + return ret + + } +} diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Home/HorizontalPlaces.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Home/HorizontalPlaces.swift index b0803a4200..8fe497e5f2 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Home/HorizontalPlaces.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Home/HorizontalPlaces.swift @@ -45,56 +45,57 @@ struct Place: View { var body: some View { ZStack() { - LoadImageView(url: place.cover) - - VStack { - Spacer() - HStack() { - VStack(alignment: .leading) { - Text(place.name) - .font(.semiBold(size: 15)) - .foregroundColor(.white) - .lineLimit(2) - VerticalSpace(height: 4) - - HStack(alignment: .center) { - Text(String(format: "%.1f", place.rating ?? 0.0)) - .font(.semiBold(size: 15)) - .foregroundColor(.white) - Image(systemName: "star.fill") - .resizable() - .foregroundColor(Color.starYellow) - .frame(width: 10, height: 10) - } - } - .padding(12) - - Spacer() - } - .frame(width: width) - .background(SwiftUI.Color.black.opacity(0.5)) - } - - HStack { - Spacer() + LoadImageView(url: place.cover) + VStack { - Button(action: { - onFavoriteChanged(!isFavorite) - }) { - Image(systemName: isFavorite ? "heart.fill" : "heart") - .foregroundColor(.white) - .padding(12) - .background(SwiftUI.Color.white.opacity(0.2)) - .clipShape(Circle()) - } - Spacer() + Spacer() + HStack() { + VStack(alignment: .leading) { + Text(place.name) + .font(.semiBold(size: 15)) + .foregroundColor(.white) + .lineLimit(2) + VerticalSpace(height: 4) + + HStack(alignment: .center) { + Text(String(format: "%.1f", place.rating ?? 0.0)) + .font(.semiBold(size: 15)) + .foregroundColor(.white) + Image(systemName: "star.fill") + .resizable() + .foregroundColor(Color.starYellow) + .frame(width: 10, height: 10) + } + } + .padding(12) + + Spacer() + } + .frame(width: width) + .background(SwiftUI.Color.black.opacity(0.5)) } - } - .padding(12) - .frame(width: width, height: height) + + HStack { + Spacer() + VStack { + Button(action: { + onFavoriteChanged(!isFavorite) + }) { + Image(systemName: isFavorite ? "heart.fill" : "heart") + .foregroundColor(.white) + .padding(12) + .background(SwiftUI.Color.white.opacity(0.2)) + .clipShape(Circle()) + } + Spacer() + } + } + .padding(12) + .frame(width: width, height: height) } .frame(width: width, height: height) .clipShape(RoundedRectangle(cornerRadius: 16)) + .contentShape(Rectangle()) // Add this line .onTapGesture(perform: onPlaceClick) } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PersonalDataViewController.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PersonalDataViewController.swift index 89b4642ca9..2e6b1d95ed 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PersonalDataViewController.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PersonalDataViewController.swift @@ -44,13 +44,15 @@ struct PersonalDataScreen: View { // pfp Group { if profileVM.isImagePickerUsed { - Image(uiImage: profileVM.pfpToUpload).resizable() + Image(uiImage: profileVM.pfpToUpload) + .resizable() } else { LoadImageView(url: profileVM.pfpFromRemote?.absoluteString) } } .scaledToFill() .frame(width: 100, height: 100) + .background(Color.surface) .clipShape(Circle()) Spacer().frame(width: 32) diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/AllPicsScreen.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/AllPicsScreen.swift index dbfa92b5e5..dbc97995f0 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/AllPicsScreen.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/AllPicsScreen.swift @@ -31,6 +31,10 @@ struct AllPicsScreen: View { } } .padding(.horizontal, 16) + .padding(.top, UIApplication.shared.statusBarFrame.height) + .padding(.bottom, 48) + .background(Color.background) + .ignoresSafeArea() } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/PlaceViewController.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/PlaceViewController.swift index 801b2d2df6..4c6fdbad1e 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/PlaceViewController.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/PlaceViewController.swift @@ -39,6 +39,7 @@ class PlaceViewController: UIViewController { struct PlaceScreen: View { @ObservedObject var placeVM: PlaceViewModel + let reviewsVM: ReviewsViewModel let id: Int64 let showBottomBar: () -> Void @@ -46,6 +47,21 @@ struct PlaceScreen: View { @Environment(\.presentationMode) var presentationMode: Binding + init(placeVM: PlaceViewModel, id: Int64, showBottomBar: @escaping () -> Void) { + self.placeVM = placeVM + self.id = id + self.showBottomBar = showBottomBar + + self.reviewsVM = ReviewsViewModel( + reviewsRepository: ReviewsRepositoryImpl( + reviewsPersistenceController: ReviewsPersistenceController.shared, + reviewsService: ReviewsServiceImpl(userPreferences: UserPreferences.shared) + ), + userPreferences: UserPreferences.shared, + id: id + ) + } + var body: some View { if let place = placeVM.place { VStack { @@ -79,7 +95,11 @@ struct PlaceScreen: View { .tag(0) GalleryScreen(urls: place.pics) .tag(1) - ReviewsScreen(placeId: place.id, rating: place.rating) + ReviewsScreen( + reviewsVM: reviewsVM, + placeId: place.id, + rating: place.rating + ) .tag(2) } .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/AllReviewsScreen.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/AllReviewsScreen.swift index 3990eac06c..e664742d75 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/AllReviewsScreen.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/AllReviewsScreen.swift @@ -19,6 +19,10 @@ struct AllReviewsScreen: View { } } .padding(.horizontal, 16) + .padding(.top, UIApplication.shared.statusBarFrame.height) + .padding(.bottom, 48) + .background(Color.background) + .ignoresSafeArea() } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/Components/PostReviewView.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/Components/PostReviewView.swift index 95c0e17810..5aa374e296 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/Components/PostReviewView.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/Components/PostReviewView.swift @@ -9,6 +9,14 @@ struct PostReviewView: View { @State private var showImagePicker = false + @State private var message = "" + @State private var showMessage = false + + private func showMessage(_ message: String) { + self.message = message + self.showMessage = true + } + var body: some View { ScrollView { VStack { @@ -30,21 +38,20 @@ struct PostReviewView: View { Spacer().frame(height: 16) // Display the selected images - FlowStack(data: postReviewVM.files, spacing: 16, alignment: .center) { file in + FlowStack(data: postReviewVM.images, spacing: 16, alignment: .center) { file in ImagePreviewView(image: file) { postReviewVM.removeFile(file) } } Spacer().frame(height: 32) - if(postReviewVM.files.count < 10) { + if(postReviewVM.images.count < 10) { VStack(alignment: .leading) { PrimaryButton( label: L("upload_photo"), onClick: { showImagePicker = true - }, - isLoading: postReviewVM.isPosting + } ) Spacer().frame(height: 4) Text(L("images_number_warning")) @@ -68,14 +75,22 @@ struct PostReviewView: View { .onReceive(postReviewVM.uiEvents) { event in switch event { case .closeReviewBottomSheet: - onPostReviewSuccess() + onPostReviewSuccess() case .showToast(let message): - // TODO: cmon - print(message) + showMessage(message) } } + .overlay( + Group { + if showMessage { + ToastView(message: message, isPresented: $showMessage) + .padding(.bottom) + } + }, + alignment: .bottom + ) .sheet(isPresented: $showImagePicker) { - MultiImagePicker(selectedImages: $postReviewVM.files) + MultiImagePicker(selectedImages: $postReviewVM.images) } } } @@ -103,32 +118,32 @@ struct ImagePreviewView: View { struct MultilineTextField: View { - @Binding var text: String - let placeholder: String - let minHeight: CGFloat - - init(_ placeholder: String, text: Binding, minHeight: CGFloat = 100) { - self._text = text - self.placeholder = placeholder - self.minHeight = minHeight - } - - var body: some View { - ZStack(alignment: .topLeading) { - TextEditor(text: $text) - .frame(minHeight: minHeight) - .padding(4) - - if text.isEmpty { - Text(placeholder) - .foregroundColor(SwiftUI.Color(.placeholderText)) - .padding(.horizontal, 8) - .padding(.vertical, 12) - } - } - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(SwiftUI.Color.gray.opacity(0.2), lineWidth: 1) - ) + @Binding var text: String + let placeholder: String + let minHeight: CGFloat + + init(_ placeholder: String, text: Binding, minHeight: CGFloat = 100) { + self._text = text + self.placeholder = placeholder + self.minHeight = minHeight + } + + var body: some View { + ZStack(alignment: .topLeading) { + TextEditor(text: $text) + .frame(minHeight: minHeight) + .padding(4) + + if text.isEmpty { + Text(placeholder) + .foregroundColor(SwiftUI.Color(.placeholderText)) + .padding(.horizontal, 8) + .padding(.vertical, 12) + } } + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(SwiftUI.Color.gray.opacity(0.2), lineWidth: 1) + ) + } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/Components/ReviewView.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/Components/ReviewView.swift index 095828cea5..f24f91c551 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/Components/ReviewView.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/Components/ReviewView.swift @@ -1,5 +1,4 @@ import SwiftUI -import SDWebImageSwiftUI struct ReviewView: View { let review: Review @@ -12,8 +11,10 @@ struct ReviewView: View { Divider() HStack { - UserView(user: review.user) - Spacer() + if let user = review.user { + UserView(user: user) + Spacer() + } if review.deletionPlanned { Text(L("deletionPlanned")) .textStyle(TextStyle.b2) @@ -61,15 +62,14 @@ struct UserView: View { var body: some View { HStack { if let pfpUrl = user.pfpUrl { - WebImage(url: URL(string: pfpUrl)) - .resizable() - .aspectRatio(contentMode: .fill) + LoadImageView(url: pfpUrl) .frame(width: 66, height: 66) + .background(Color.surface) .clipShape(Circle()) } HStack() { Text(user.name) - .textStyle(TextStyle.h3) + .textStyle(TextStyle.h4) .foregroundColor(Color.onBackground) UICountryFlagView(code: user.countryCodeName) .scaledToFit() @@ -132,10 +132,9 @@ struct ReviewPicView: View { let url: String var body: some View { - WebImage(url: URL(string: url)) - .resizable() - .aspectRatio(contentMode: .fill) + LoadImageView(url: url) .frame(width: reviewPicWidth, height: reviewPicHeight) + .background(Color.surface) .clipShape(RoundedRectangle(cornerRadius: 8)) } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/PostReviewViewModel.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/PostReviewViewModel.swift index 208acda29e..9795b21888 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/PostReviewViewModel.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/PostReviewViewModel.swift @@ -3,56 +3,58 @@ import SwiftUI import Combine class PostReviewViewModel: ObservableObject { + private var cancellables = Set() + + private let reviewsRepository: ReviewsRepository + @Published var rating: Double = 5 @Published var comment: String = "" - @Published var files: [UIImage] = [] + @Published var images: [UIImage] = [] @Published var isPosting: Bool = false - private var cancellables = Set() - // private let reviewsRepository: ReviewsRepository + @Published var retrievedImages: [UIImage] = [] let uiEvents = PassthroughSubject() - // init(reviewsRepository: ReviewsRepository) { - // self.reviewsRepository = reviewsRepository - // } + init(reviewsRepository: ReviewsRepository) { + self.reviewsRepository = reviewsRepository + } func setRating(_ value: Double) { rating = value } - func addPickedImage() { - // guard let pickedImage = pickedImage else { return } - // Task { - // if let data = try? await pickedImage.loadTransferable(type: Data.self), let image = UIImage(data: data) { - // files.append(image) - // } - // } - } - func removeFile(_ file: UIImage) { - files.removeAll { $0 == file } + images.removeAll { $0 == file } } func postReview(placeId: Int64) { - // isPosting = true - // - // let review = ReviewToPost(placeId: placeId, comment: comment, rating: rating, images: files) - // reviewsRepository.postReview(review) - // .receive(on: DispatchQueue.main) - // .sink { completion in - // self.isPosting = false - // switch completion { - // case .finished: - // self.uiEvents.send(.showToast(message: "Review Posted")) - // self.uiEvents.send(.closeReviewBottomSheet) - // case .failure(let error): - // self.uiEvents.send(.showToast(message: error.localizedDescription)) - // } - // } receiveValue: { response in - // print("Review posted successfully") - // } - // .store(in: &cancellables) + isPosting = true + + let urls = saveMultipleImages(images, placeId: placeId) + print(urls) + let review = ReviewToPost( + placeId: placeId, + comment: comment, + rating: Int(rating), + images: urls + ) + + reviewsRepository.postReview(review: review) + .receive(on: DispatchQueue.main) + .sink { completion in + self.isPosting = false + switch completion { + case .finished: + self.uiEvents.send(.showToast(message: "Review Posted")) + self.uiEvents.send(.closeReviewBottomSheet) + case .failure(let error): + self.uiEvents.send(.showToast(message: error.errorDescription)) + } + } receiveValue: { response in + print(response) + } + .store(in: &cancellables) } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsScreen.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsScreen.swift index 52677b97d1..79d1959dd6 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsScreen.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsScreen.swift @@ -1,11 +1,21 @@ import SwiftUI struct ReviewsScreen: View { -// @ObservedObject var reviewsVM = ReviewsViewModel() + @ObservedObject var reviewsVM: ReviewsViewModel let placeId: Int64 let rating: Double? + init( + reviewsVM: ReviewsViewModel, + placeId: Int64, + rating: Double? + ) { + self.reviewsVM = reviewsVM + self.placeId = placeId + self.rating = rating + } + @State private var showReviewSheet = false var body: some View { @@ -35,31 +45,44 @@ struct ReviewsScreen: View { HStack { Spacer() -// NavigationLink(destination: AllReviewsScreen(reviewsVM: reviewsVM)) { -// Text(L("see_all_reviews")) -// .foregroundColor(Color.primary) -// } + NavigationLink(destination: AllReviewsScreen(reviewsVM: reviewsVM)) { + Text(L("see_all_reviews")) + .foregroundColor(Color.primary) + } } // user review - ReviewView( - review: Constants.reviewExample, - onDeleteClick: {} - ) + if let userReview = reviewsVM.userReview, !reviewsVM.isThereReviewPlannedToPublish { + ReviewView( + review: userReview, + onDeleteClick: { + reviewsVM.deleteReview() + } + ) + } // most recent recent review - ReviewView( - review: Constants.reviewExample, - onDeleteClick: nil - ) + if let mostRecentReview = reviewsVM.latestReview { + ReviewView( + review: mostRecentReview, + onDeleteClick: nil + ) + } } } .padding(.horizontal, 16) .sheet(isPresented: $showReviewSheet) { PostReviewView( - postReviewVM: PostReviewViewModel(), - placeId: placeId) { - // TODO: cmon + postReviewVM: PostReviewViewModel( + reviewsRepository: ReviewsRepositoryImpl( + reviewsPersistenceController: ReviewsPersistenceController.shared, + reviewsService: ReviewsServiceImpl(userPreferences: UserPreferences.shared) + ) + ), + placeId: placeId, + onPostReviewSuccess: { + showReviewSheet = false } + ) } } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsViewModel.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsViewModel.swift index 1af97c8f9f..d81547accb 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsViewModel.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsViewModel.swift @@ -1,32 +1,91 @@ import Combine class ReviewsViewModel: ObservableObject { - private let cancellables = Set() + private var cancellables = Set() private let reviewsRepository: ReviewsRepository + private let userPreferences: UserPreferences - init(reviewsRepository: ReviewsRepository, id: Int64) { - self.reviewsRepository = reviewsRepository - - observeReviews(id: id) - - // TODO: cmon get isThereReviewPlannedToPublish from DB - } + private let placeId: Int64 - @Published var reviews: [Review] = [Constants.reviewExample] + @Published var messageToShowOnReviewsScreen = "" + @Published var shouldShowMessageOnReviewsScreen = false + + @Published var reviews: [Review] = [] @Published var userReview: Review? = nil + @Published var latestReview: Review? = nil @Published var isThereReviewPlannedToPublish = false - func observeReviews(id: Int64) { + init(reviewsRepository: ReviewsRepository, userPreferences: UserPreferences, id: Int64) { + self.reviewsRepository = reviewsRepository + self.userPreferences = userPreferences + self.placeId = id + + observeReviews() + observeIfThereIsReviewPlannedToPost() + } + + func observeReviews() { + reviewsRepository.observeReviewsForPlace(id: placeId) + reviewsRepository.reviewsResource.sink { _ in } receiveValue: { reviews in self.reviews = reviews + + self.getUserReview() + self.getLatestReview() } - + .store(in: &cancellables) + } + + private func getUserReview() { + let userId = userPreferences.getUserId() + let first = reviews.filter { + if let user = $0.user { + return String(user.id) == userId + } else { + return false + } + }.first - reviewsRepository.observeReviewsForPlace(id: id) + if let userReview = first { + self.userReview = userReview + } + } + + private func getLatestReview() { + if let latest = reviews.first { + self.latestReview = latest + } + } + + private func observeIfThereIsReviewPlannedToPost() { + reviewsRepository.checkIfThereIsReviewPlannedToPublish(for: placeId) + + reviewsRepository.isThereReviewPlannedToPublishResource.sink { _ in } receiveValue: { isThere in + self.isThereReviewPlannedToPublish = isThere + } + .store(in: &cancellables) } func deleteReview() { - // TODO: cmon + if let id = userReview?.id { + reviewsRepository.deleteReview(id: id) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + self.userReview = nil + case .failure(let error): + self.showMessageOnReviewsScreen(error.localizedDescription) + } + }, receiveValue: { response in + self.showMessageOnReviewsScreen(response.message) + }) + .store(in: &cancellables) + } + } + + func showMessageOnReviewsScreen(_ message: String) { + messageToShowOnReviewsScreen = message + shouldShowMessageOnReviewsScreen = true } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewController.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewController.swift index 42b4a07e38..243fb4f53b 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewController.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewController.swift @@ -126,6 +126,7 @@ struct ProfileBar: View { HStack(alignment: .center) { LoadImageView(url: personalData.pfpUrl) .frame(width: 100, height: 100) + .background(Color.surface) .clipShape(Circle()) HorizontalSpace(width: 16) diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewModel.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewModel.swift index 78de040762..04ddf25e5b 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewModel.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewModel.swift @@ -95,8 +95,7 @@ class ProfileViewModel: ObservableObject { profileRepository.updateProfile( fullName: fullName, country: countryCodeName!, - // We shouldn't send email field if there's no change - email: email == currentEmail ? nil : email, + email: email, pfpUrl: pfpToUpload ) .sink { completion in diff --git a/iphone/Maps/Tourism/Presentation/Home/TabBarController.swift b/iphone/Maps/Tourism/Presentation/Home/TabBarController.swift index 756b84d603..bb4fc41082 100644 --- a/iphone/Maps/Tourism/Presentation/Home/TabBarController.swift +++ b/iphone/Maps/Tourism/Presentation/Home/TabBarController.swift @@ -44,6 +44,17 @@ class TabBarController: UITabBarController { ) let authRepository = AuthRepositoryImpl(authService: AuthServiceImpl()) + // monitoring network for sync + let dataSyncer = DataSyncer( + reviewsRepository: ReviewsRepositoryImpl( + reviewsPersistenceController: ReviewsPersistenceController.shared, + reviewsService: ReviewsServiceImpl(userPreferences: UserPreferences.shared) + ), + placesRepository: placesRepository + ) + dataSyncer.startMonitoring() + + // creating shared viewModels() let homeVM = HomeViewModel(placesRepository: placesRepository) let categoriesVM = CategoriesViewModel(placesRepository: placesRepository) let favoritesVM = FavoritesViewModel(placesRepository: placesRepository) diff --git a/iphone/Maps/Tourism/Utils/ImageStoreUtils.swift b/iphone/Maps/Tourism/Utils/ImageStoreUtils.swift new file mode 100644 index 0000000000..5fdc369ede --- /dev/null +++ b/iphone/Maps/Tourism/Utils/ImageStoreUtils.swift @@ -0,0 +1,33 @@ +import UIKit + +func saveMultipleImages(_ images: [UIImage], placeId: Int64) -> [URL] { + let fileManager = FileManager.default + let documentsDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] + + return images.enumerated().compactMap { (index, image) in + let fileName = "image_\(index)_placeId\(placeId).jpg" + let fileURL = documentsDirectory.appendingPathComponent(fileName) + + guard let data = image.jpegData(compressionQuality: 0.01) else { return nil } + + do { + try data.write(to: fileURL) + return fileURL + } catch { + print("Error saving image \(fileName): \(error)") + return nil + } + } +} + +func retrieveMultipleImages(urls: [URL]) -> [UIImage] { + return urls.compactMap { url in + do { + let imageData = try Data(contentsOf: url) + return UIImage(data: imageData) + } catch { + print("Error retrieving image at \(url): \(error)") + return nil + } + } +}