Created
October 25, 2021 20:20
-
-
Save saroar/abdb5ec787246d150e66be1a4cb7d03b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// EventReducer.swift | |
// | |
// | |
// Created by Saroar Khandoker on 06.04.2021. | |
// | |
import ChatClient | |
import ChatClientLive | |
import ChatView | |
import Combine | |
import ComposableArchitecture | |
import ComposableArchitectureHelpers | |
import ComposableCoreLocation | |
import Contacts | |
import ConversationClient | |
import ConversationClientLive | |
import EventClient | |
import EventClientLive | |
import EventDetailsView | |
import EventFormView | |
import HTTPRequestKit | |
import MapKit | |
import SharedModels | |
import SwiftUI | |
import WebSocketClient | |
import WebSocketClientLive | |
struct LocationManagerId: Hashable {} | |
// swiftlint:disable superfluous_disable_command file_length | |
public let eventReducer = Reducer<EventsState, EventsAction, EventsEnvironment> { | |
state, action, environment in | |
var fetchEvents: Effect<EventsAction, Never> { | |
guard state.isConnected, let location = state.location else { return .none } | |
state.location = location | |
guard !state.isLoadingPage, state.canLoadMorePages else { return .none } | |
// state.isLoadingPage = true | |
let getDistanceType = environment.userDefaults.integerForKey("typee") | |
let maxDistance = getDistanceType == 0 ? (250 * 1000) : (250 / 1.609) * 1609 | |
let distanceType: String = getDistanceType == 0 ? "kilometers" : "miles" | |
let getDistance = environment.userDefaults.doubleForKey(distanceType) | |
var distanceInMeters: Double = 0.0 | |
if getDistance != 0.0 { | |
if getDistanceType == 0 { | |
distanceInMeters = getDistance * 1000 | |
} else { | |
distanceInMeters = getDistance * 1609 | |
} | |
} else { | |
if distanceType == "kilometers" { | |
distanceInMeters = maxDistance | |
} else { | |
distanceInMeters = maxDistance | |
} | |
} | |
let lat = "\(location.coordinate.latitude)" | |
let long = "\(location.coordinate.longitude)" | |
print(#line, distanceInMeters) | |
let query = QueryItem( | |
page: "\(state.currentPage)", | |
per: "10", lat: lat, long: long, | |
distance: "\(Int(distanceInMeters))" | |
) | |
return environment.eventClient.events(query, "") | |
.retry(3) | |
.receive(on: environment.mainQueue.animation(.default)) | |
.removeDuplicates() | |
.catchToEffect() | |
.map(EventsAction.eventsResponse) | |
} | |
func getLocation(_ location: Location) -> Effect<EventsAction, Never> { | |
return environment.getCoordinate(location) | |
.receive(on: environment.mainQueue) | |
.catchToEffect() | |
.map(EventsAction.eventCoordinate) | |
} | |
func presentChatView() -> Effect<EventsAction, Never> { | |
state.eventDetailsState = nil | |
state.chatState = nil | |
return Effect(value: EventsAction.chatView(isNavigate: true)) | |
.receive(on: environment.mainQueue) | |
.eraseToEffect() | |
} | |
switch action { | |
case .onAppear: | |
return .merge( | |
environment.locationManager | |
.delegate() | |
.map(EventsAction.locationManager), | |
environment.locationManager | |
.requestWhenInUseAuthorization() | |
.fireAndForget() | |
) | |
case .dismissEvent: | |
return .none | |
case .alertDismissed: | |
state.alert = nil | |
return .none | |
case let .fetchMoreEventsIfNeeded(item): | |
guard let item = item, state.events.count > 5 else { | |
return fetchEvents | |
} | |
let threshouldIndex = state.events.index(state.events.endIndex, offsetBy: -5) | |
if state.events.firstIndex(where: { $0.id == item.id }) == threshouldIndex { | |
return fetchEvents | |
} | |
return .none | |
case let .event(index: index): | |
return .none | |
case let .eventsResponse(.success(eventArray)): | |
state.waitingForUpdateLocation = false | |
state.canLoadMorePages = state.events.count < eventArray.metadata.total | |
state.isLoadingPage = false | |
state.currentPage += 1 | |
let events = (state.events + eventArray.items) | |
// .uniqElemets() | |
// .sorted() | |
state.events = .init(uniqueElements: events) | |
return .none | |
case let .eventsResponse(.failure(error)): | |
state.isLoadingPage = false | |
state.alert = .init(title: TextState(error.description)) | |
return .none | |
case let .eventTapped(event): | |
state.event = event | |
return Effect(value: EventsAction.eventDetailsView(isPresented: true)) | |
.receive(on: environment.mainQueue) | |
.eraseToEffect() | |
case let .addressResponse(.success(address)): | |
return .none | |
case let .eventCoordinate(.success(placemark)): | |
let formatter = CNPostalAddressFormatter() | |
guard let postalAddress = placemark.postalAddress else { | |
// handle error here | |
return .none | |
} | |
let addressString = formatter.string(from: postalAddress) | |
state.currentAddress = addressString | |
state.placeMark = placemark | |
return .none | |
case let .locationManager(.didUpdateLocations(locations)): | |
guard state.isConnected, let location = locations.first else { return .none } | |
state.location = location | |
return .merge( | |
fetchEvents, | |
getLocation(location) | |
) | |
case .locationManager: | |
return .none | |
case .currentLocationButtonTapped: | |
guard environment.locationManager.locationServicesEnabled() else { | |
state.alert = .init(title: TextState("Location services are turned off.")) | |
state.waitingForUpdateLocation = false | |
return .none | |
} | |
switch environment.locationManager.authorizationStatus() { | |
case .notDetermined: | |
state.isRequestingCurrentLocation = true | |
state.waitingForUpdateLocation = false | |
#if os(macOS) | |
return environment.locationManager | |
.requestAlwaysAuthorization() | |
.fireAndForget() | |
#else | |
return environment.locationManager | |
.requestWhenInUseAuthorization() | |
.fireAndForget() | |
#endif | |
case .restricted: | |
state.alert = .init( | |
title: .init("Please give us access to your location in settings"), | |
message: .init("Please go to Settings and turn on the permissions"), | |
primaryButton: .cancel(.init("Cancel"), action: .send(.alertDismissed)), | |
secondaryButton: .default(.init(""), action: .send(.popupSettings)) | |
) | |
state.waitingForUpdateLocation = false | |
return .none | |
case .denied: | |
state.alert = .init( | |
title: .init("Please give us access to your location in settings"), | |
message: .init("Please go to Settings and turn on the permissions"), | |
primaryButton: .cancel(.init("Cancel"), action: .send(.alertDismissed)), | |
secondaryButton: .default(TextState(""), action: .send(.popupSettings)) | |
) | |
state.waitingForUpdateLocation = false | |
return .none | |
case .authorizedAlways, .authorizedWhenInUse: | |
state.isLocationAuthorized = true | |
state.isConnected = true | |
state.waitingForUpdateLocation = false | |
return environment.locationManager | |
.requestLocation() | |
.fireAndForget() | |
@unknown default: | |
return .none | |
} | |
case .popupSettings: | |
// @available(iOSApplicationExtension, unavailable) | |
// if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) { | |
// UIApplication.shared.open(url, options: [:], completionHandler: nil) | |
// } | |
return .none | |
case .dismissEventDetails: | |
state.event = nil | |
state.eventDetailsState = nil | |
return .none | |
case let .eventFormView(isNavigate: active): | |
guard | |
let placeMark = state.placeMark, | |
let location = placeMark.location | |
else { | |
// pop alert let user know about issue | |
return .none | |
} | |
state.eventFormState = | |
active | |
? EventFormState( | |
placeMark: state.placeMark, | |
eventAddress: state.currentAddress, | |
eventCoordinate: location.coordinate | |
) | |
: nil | |
return .none | |
case let .eventForm(.eventsResponse(.success(event))): | |
state.events.insert(event, at: 0) | |
return .none | |
case .eventForm(.backToPVAfterCreatedEventSuccessfully): | |
state.eventFormState = nil | |
return .none | |
case .eventForm: | |
return .none | |
case let .chat(isNavigate): | |
return .none | |
case let .eventDetailsView(isPresented: present): | |
guard let event = state.event else { return .none } | |
if present { | |
let eventDetailsOverlayState = EventDetailsOverlayState(alert: nil, event: event) | |
state.eventDetailsState = EventDetailsState( | |
event: event, | |
eventDetailsOverlayState: eventDetailsOverlayState | |
) | |
} else { | |
state.eventDetailsState = nil | |
state.event = nil | |
} | |
return .none | |
case let .eventDetails(action): | |
switch action { | |
case .onAppear, .alertDismissed, .moveToChatRoom(_), .updateRegion: | |
return .none | |
case let .eventDetailsOverlay(eventDetailsAction): | |
switch eventDetailsAction { | |
case .onAppear, .alertDismissed: | |
return .none | |
case let .startChat(present): | |
return presentChatView() | |
case let .askJoinRequest(bool): | |
state.isMovingChatRoom = bool | |
return .none | |
case let .joinToEvent(.success(string)): // joinToEventRequest | |
return presentChatView() | |
case let .joinToEvent(.failure(error)): // joinToEventRequest | |
return .none | |
case let .conversationResponse(.success(conversationItem)): | |
state.conversation = conversationItem | |
return .none | |
case let .conversationResponse(.failure(error)): | |
return .none | |
} | |
} | |
case let .chatView(isNavigate: isNavigate): | |
state.chatState = isNavigate ? ChatState(conversation: state.conversation) : nil | |
return .none | |
} | |
} | |
.combined( | |
with: locationManagerReducer | |
.pullback(state: \.self, action: /EventsAction.locationManager, environment: { $0 }) | |
) | |
.signpost() | |
.debug() | |
.presents( | |
eventFormReducer, | |
state: \.eventFormState, | |
action: /EventsAction.eventForm, | |
environment: { | |
EventFormEnvironment( | |
eventClient: EventClient.live(api: .build), | |
mainQueue: $0.mainQueue | |
) | |
} | |
) | |
.presents( | |
chatReducer, | |
state: \.chatState, | |
action: /EventsAction.chat, | |
environment: { | |
ChatEnvironment( | |
chatClient: ChatClient.live(api: .build), | |
websocketClient: .live, | |
mainQueue: $0.mainQueue, | |
backgroundQueue: $0.backgroundQueue | |
) | |
} | |
) | |
.presents( | |
eventDetailsReducer, | |
state: \.eventDetailsState, | |
action: /EventsAction.eventDetails, | |
environment: { | |
EventDetailsEnvironment( | |
conversationClient: ConversationClient.live(api: .build), | |
mainQueue: $0.mainQueue | |
) | |
} | |
) | |
private let locationManagerReducer = Reducer< | |
EventsState, LocationManager.Action, EventsEnvironment | |
> { state, action, environment in | |
switch action { | |
case .didChangeAuthorization(.authorizedAlways), | |
.didChangeAuthorization(.authorizedWhenInUse): | |
state.isLocationAuthorized = true | |
state.isConnected = true | |
return environment.locationManager | |
.requestLocation() | |
.fireAndForget() | |
case .didChangeAuthorization(.denied), | |
.didChangeAuthorization(.restricted): | |
state.waitingForUpdateLocation = false | |
state.isLocationAuthorized = false | |
state.isConnected = false | |
state.alert = .init( | |
title: TextState("Please give us access to your location so you can use our full features"), | |
message: TextState("Please go to Settings and turn on the permissions"), | |
primaryButton: .cancel(.init("Cancel"), action: .send(.alertDismissed)), | |
secondaryButton: .default(.init("Go Settings"), action: .send(.popupSettings)) | |
) | |
return .none | |
case let .didUpdateLocations(locations): | |
state.isRequestingCurrentLocation = false | |
return .none | |
default: | |
return .none | |
} | |
} | |
extension MKPlacemark { | |
var formattedAddress: String? { | |
guard let postalAddress = postalAddress else { return nil } | |
return CNPostalAddressFormatter.string( | |
from: postalAddress, style: .mailingAddress | |
) | |
.replacingOccurrences(of: "\n", with: " ") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment