android: disable car support, sent to Play Store for review; ios: backup

This commit is contained in:
Emin 2024-09-18 22:59:57 +05:00
parent 52bf2acb91
commit 5ae27cbccb
54 changed files with 871 additions and 350 deletions

View file

@ -31,3 +31,6 @@
# ignore autogenerated metadata (see prepareGoogleReleaseListing in build.gradle)
/src/google/play/listings
# ignore google releases
/google/release

View file

@ -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

View file

@ -44,8 +44,8 @@
//
-->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES" />
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
<!-- <uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES" />-->
<!-- <uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />-->
<queries>
<intent>
@ -444,16 +444,16 @@
android:label="@string/driving_options_title" />
<activity android:name=".MapPlaceholderActivity" />
<service
android:name=".car.CarAppService"
android:exported="true"
android:foregroundServiceType="location">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<!-- <service-->
<!-- android:name=".car.CarAppService"-->
<!-- android:exported="true"-->
<!-- android:foregroundServiceType="location">-->
<!-- <intent-filter>-->
<!-- <action android:name="androidx.car.app.CarAppService" />-->
<category android:name="androidx.car.app.category.NAVIGATION" />
</intent-filter>
</service>
<!-- <category android:name="androidx.car.app.category.NAVIGATION" />-->
<!-- </intent-filter>-->
<!-- </service>-->
<service
android:name=".routing.NavigationService"
android:enabled="true"
@ -483,12 +483,12 @@
<meta-data
android:name="com.samsung.android.multidisplay.keep_process_alive"
android:value="true" />
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="5" />
<!-- <meta-data-->
<!-- android:name="com.google.android.gms.car.application"-->
<!-- android:resource="@xml/automotive_app_desc" />-->
<!-- <meta-data-->
<!-- android:name="androidx.car.app.minCarApiLevel"-->
<!-- android:value="5" />-->
</application>
</manifest>

View file

@ -47,6 +47,6 @@ interface ReviewsDao {
@Query("SELECT * FROM reviews_planned_to_post")
fun getReviewsPlannedToPost(): List<ReviewPlannedToPostEntity>
@Query("SELECT * FROM reviews_planned_to_post")
fun getReviewsPlannedToPostFlow(): Flow<List<ReviewPlannedToPostEntity>>
@Query("SELECT * FROM reviews_planned_to_post WHERE placeId = :placeId")
fun getReviewsPlannedToPostFlow(placeId: Long): Flow<List<ReviewPlannedToPostEntity>>
}

View file

@ -41,8 +41,8 @@ class ReviewsRepository(
}
}
fun isThereReviewPlannedToPublish(): Flow<Boolean> = channelFlow {
reviewsDao.getReviewsPlannedToPostFlow().collectLatest { reviewsEntities ->
fun isThereReviewPlannedToPublish(placeId: Long): Flow<Boolean> = 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)))

View file

@ -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<CountryCodePicker>(R.id.ccp)
ccp.contentColor = contentColor
ccp.setCountryForNameCode("BO")
ccp.setCountryForNameCode(countryCodeName)
ccp.showArrow(false)
ccp.setCcpClickable(false)

View file

@ -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)

View file

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

View file

@ -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)
}
}

View file

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

View file

@ -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(),
)
}
}
}

View file

@ -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) {

View file

@ -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">

View file

@ -2226,7 +2226,7 @@
<string name="deletionPlanned">В процессе удаления</string>
<string name="plz_wait_dowloading">Пожалуйста подождите данные скачиваются</string>
<string name="empty_list">Пусто</string>
<string name="review_will_be_published">Отзыв будет публикован когда будете онлайн</string>
<string name="review_will_be_published_when_online">Отзыв будет публикован когда будете онлайн</string>
<string name="review_was_published">Отзыв был успешно опубликован</string>
<string name="failed_to_publish_review">Не удалось публиковать отзыв</string>
<string name="plz_dont_go_out_of_tjk">Поажалуйста, не выходите за рамки Таджикистана, вы должны быть в Таджикистане</string>

View file

@ -2268,7 +2268,7 @@
<string name="deletionPlanned">Deleting…</string>
<string name="plz_wait_dowloading">Please, wait, data being downloaded</string>
<string name="empty_list">Пусто</string>
<string name="review_will_be_published">Review will be published when you are online</string>
<string name="review_will_be_published_when_online">Review will be published when you are online</string>
<string name="review_was_published">Review was successfully published</string>
<string name="failed_to_publish_review">Failed to publish review\n</string>
<string name="plz_dont_go_out_of_tjk">Please, don\'t go out of Tajikistan, it\'s Tajikistan app</string>

View file

@ -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";

View file

@ -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";

View file

@ -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" = "Отзыв был успешно опубликован";

View file

@ -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 = "<group>"; };
529A5F1D2C86DDE5004FE4A1 /* PlaceDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceDTO.swift; sourceTree = "<group>"; };
529A5F1F2C86DE14004FE4A1 /* CoordinatesDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatesDTO.swift; sourceTree = "<group>"; };
529A5F212C86DE50004FE4A1 /* ReviewDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewDTO.swift; sourceTree = "<group>"; };
529A5F232C86DE7D004FE4A1 /* ReviewIdsDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewIdsDTO.swift; sourceTree = "<group>"; };
529A5F252C86DE9D004FE4A1 /* ReviewsDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsDTO.swift; sourceTree = "<group>"; };
529A5F212C86DE50004FE4A1 /* Reviews DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Reviews DTOs.swift"; sourceTree = "<group>"; };
529A5F272C86DEC5004FE4A1 /* UserDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDTO.swift; sourceTree = "<group>"; };
529A5F2A2C86DF2D004FE4A1 /* PlaceShort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceShort.swift; sourceTree = "<group>"; };
529A5F2C2C86DF3B004FE4A1 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
529A5F2E2C86DF51004FE4A1 /* ReviewToPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewToPost.swift; sourceTree = "<group>"; };
529A5F302C86DF61004FE4A1 /* Review.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Review.swift; sourceTree = "<group>"; };
529A5F302C86DF61004FE4A1 /* Review Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review Models.swift"; sourceTree = "<group>"; };
529A5F322C86DF6F004FE4A1 /* PlaceFull.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceFull.swift; sourceTree = "<group>"; };
529A5F342C86DF99004FE4A1 /* PlaceLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceLocation.swift; sourceTree = "<group>"; };
529A5F362C86E02E004FE4A1 /* AllDataDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDataDTO.swift; sourceTree = "<group>"; };
@ -1643,6 +1639,8 @@
CE64501C2C93F8350075A59B /* ReviewsPersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsPersistenceController.swift; sourceTree = "<group>"; };
CE64501F2C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsPersistenceControllerTesterBro.swift; sourceTree = "<group>"; };
CE6450232C9772310075A59B /* DownloadProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadProgress.swift; sourceTree = "<group>"; };
CE6450272C99572F0075A59B /* ImageStoreUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStoreUtils.swift; sourceTree = "<group>"; };
CEA45BC32C9AE01000ABE6B2 /* DataSyncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSyncer.swift; sourceTree = "<group>"; };
CED0E0162C8ACF0D008C61CA /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = "<group>"; };
CED0E0182C8AD57C008C61CA /* EmptyUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUI.swift; sourceTree = "<group>"; };
CED0E01A2C8B048C008C61CA /* AllPicsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPicsScreen.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 = "<group>";
@ -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 */,

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23G93" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23231" systemVersion="24A335" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="FavoriteSyncEntity" representedClassName="FavoriteSyncEntity" syncable="YES" codeGenerationType="class">
<attribute name="isFavorite" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="placeId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
@ -32,9 +32,8 @@
</entity>
<entity name="ReviewPlannedToPostEntity" representedClassName="ReviewPlannedToPostEntity" syncable="YES" codeGenerationType="class">
<attribute name="comment" attributeType="String"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="imagesJson" attributeType="String"/>
<attribute name="placeId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rating" attributeType="String"/>
<attribute name="rating" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
</model>

View file

@ -1,4 +1,4 @@
struct UserEntity: Encodable {
struct UserEntity: Codable {
let userId: Int64
let fullName: String
let avatar: String

View file

@ -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)! }
)
}
}

View file

@ -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> = FavoriteSyncEntity.fetchRequest()
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = 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> = FavoriteSyncEntity.fetchRequest()

View file

@ -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<NSFetchRequestResult> = 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> = ReviewEntity.fetchRequest()
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = 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> = 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> = ReviewPlannedToPostEntity.fetchRequest()
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = 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> = 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> = 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<NSFetchRequestResult>) {
@ -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:

View file

@ -1,5 +0,0 @@
import Foundation
struct ReviewIdsDTO: Codable {
let feedbacks: [Int64]
}

View file

@ -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]
}

View file

@ -1,5 +0,0 @@
import Foundation
struct ReviewsDTO: Codable {
let data: [ReviewDTO]
}

View file

@ -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)
}
}

View file

@ -163,7 +163,7 @@ class AppNetworkHelper {
headers: headers,
decoder: decoder
)
} catch {
} catch let error as NSError {
print(error)
throw ResourceError.other(message: "Encoding error")
}

View file

@ -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)
}
}
}
}
}

View file

@ -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(

View file

@ -1,6 +1,8 @@
import Combine
class ReviewsRepositoryImpl : ReviewsRepository {
private var cancellables = Set<AnyCancellable>()
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<SimpleResponse, ResourceError> {
// TODO: cmon
return PassthroughSubject<SimpleResponse, ResourceError>().eraseToAnyPublisher()
return Future<SimpleResponse, ResourceError> { 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<SimpleResponse, ResourceError> {
// TODO: cmon
return PassthroughSubject<SimpleResponse, ResourceError>().eraseToAnyPublisher()
return Future<SimpleResponse, ResourceError> { 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)
}
}
}
}

View file

@ -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:

View file

@ -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)
}
}

View file

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

View file

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

View file

@ -5,7 +5,7 @@ protocol ReviewsRepository {
func observeReviewsForPlace(id: Int64)
var isThereReviewPlannedToPublishResource: PassthroughSubject<Bool, Never> { get }
func isThereReviewPlannedToPublish(for placeId: Int64)
func checkIfThereIsReviewPlannedToPublish(for placeId: Int64)
func postReview(review: ReviewToPost) -> AnyPublisher<SimpleResponse, ResourceError>

View file

@ -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
}()

View file

@ -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)
}
}
}

View file

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

View file

@ -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)
}
}

View file

@ -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)

View file

@ -31,6 +31,10 @@ struct AllPicsScreen: View {
}
}
.padding(.horizontal, 16)
.padding(.top, UIApplication.shared.statusBarFrame.height)
.padding(.bottom, 48)
.background(Color.background)
.ignoresSafeArea()
}
}

View file

@ -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<PresentationMode>
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))

View file

@ -19,6 +19,10 @@ struct AllReviewsScreen: View {
}
}
.padding(.horizontal, 16)
.padding(.top, UIApplication.shared.statusBarFrame.height)
.padding(.bottom, 48)
.background(Color.background)
.ignoresSafeArea()
}
}

View file

@ -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<String>, 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<String>, 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)
)
}
}

View file

@ -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))
}
}

View file

@ -3,56 +3,58 @@ import SwiftUI
import Combine
class PostReviewViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
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<AnyCancellable>()
// private let reviewsRepository: ReviewsRepository
@Published var retrievedImages: [UIImage] = []
let uiEvents = PassthroughSubject<UiEvent, Never>()
// 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)
}
}

View file

@ -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
}
)
}
}
}

View file

@ -1,32 +1,91 @@
import Combine
class ReviewsViewModel: ObservableObject {
private let cancellables = Set<AnyCancellable>()
private var cancellables = Set<AnyCancellable>()
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
}
}

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

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