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
+ }
+ }
+}