├── .gitignore ├── Jimo ├── .gitignore ├── .swiftlint.yml ├── Jimo.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── Jimo.xcscheme ├── Jimo │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── App_store_1024_1x.png │ │ │ ├── Contents.json │ │ │ ├── iPhone_App_60_2x.png │ │ │ ├── iPhone_App_60_3x.png │ │ │ ├── iPhone_Notifications_20_2x.png │ │ │ ├── iPhone_Notifications_20_3x.png │ │ │ ├── iPhone_Settings_29_2x.png │ │ │ ├── iPhone_Settings_29_3x.png │ │ │ ├── iPhone_Spotlight_40_2x.png │ │ │ └── iPhone_Spotlight_40_3x.png │ │ ├── Contents.json │ │ ├── background.colorset │ │ │ └── Contents.json │ │ ├── categories │ │ │ ├── Contents.json │ │ │ ├── activity.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── activity.svg │ │ │ ├── attraction.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── attraction.svg │ │ │ ├── cafe.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── cafe.svg │ │ │ ├── food.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── food.svg │ │ │ ├── lodging.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── lodging.svg │ │ │ ├── nightlife.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── nightlife.svg │ │ │ └── shopping.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── Shopping bag-1.png │ │ │ │ ├── Shopping bag.png │ │ │ │ └── shopping.png │ │ ├── colors │ │ │ ├── Contents.json │ │ │ ├── activity.colorset │ │ │ │ └── Contents.json │ │ │ ├── attraction.colorset │ │ │ │ └── Contents.json │ │ │ ├── cafe.colorset │ │ │ │ └── Contents.json │ │ │ ├── food.colorset │ │ │ │ └── Contents.json │ │ │ ├── lightgray.colorset │ │ │ │ └── Contents.json │ │ │ ├── lodging.colorset │ │ │ │ └── Contents.json │ │ │ ├── nightlife.colorset │ │ │ │ └── Contents.json │ │ │ ├── shopping.colorset │ │ │ │ └── Contents.json │ │ │ └── unselected.colorset │ │ │ │ └── Contents.json │ │ ├── foreground.colorset │ │ │ └── Contents.json │ │ ├── icon.imageset │ │ │ ├── Contents.json │ │ │ ├── icon.png │ │ │ ├── icon@2x.png │ │ │ └── icon@3x.png │ │ ├── icon_white.imageset │ │ │ ├── Contents.json │ │ │ └── logo_white.svg │ │ ├── logo.imageset │ │ │ ├── Contents.json │ │ │ └── logo.svg │ │ ├── permissions │ │ │ ├── Contents.json │ │ │ ├── contacts-icon.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── contacts-icon-1.svg │ │ │ │ └── contacts-icon.svg │ │ │ ├── location-icon.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── location-icon-1.svg │ │ │ │ └── location-icon.svg │ │ │ ├── next-button.colorset │ │ │ │ └── Contents.json │ │ │ └── notifications-icon.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── notifications-icon-1.svg │ │ │ │ └── notifications-icon.svg │ │ ├── secondary.colorset │ │ │ └── Contents.json │ │ ├── selectedContact.imageset │ │ │ ├── Contents.json │ │ │ └── selectedContact.svg │ │ ├── tabs │ │ │ ├── Contents.json │ │ │ ├── feedIcon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── feedIcon.svg │ │ │ ├── mapIcon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── mapIcon.svg │ │ │ ├── postIcon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── postIcon.svg │ │ │ ├── profileIcon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── profileIcon.svg │ │ │ └── searchIcon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── searchIcon.svg │ │ ├── wallpaper │ │ │ ├── Contents.json │ │ │ ├── wallpaper.bg.colorset │ │ │ │ └── Contents.json │ │ │ ├── wallpaper.chicago.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── wallpaper.chicago.dark.heic │ │ │ │ └── wallpaper.chicago.heic │ │ │ ├── wallpaper.la.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── wallpaper.la.dark.heic │ │ │ │ └── wallpaper.la.heic │ │ │ ├── wallpaper.london.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── wallpaper.london.dark.heic │ │ │ │ └── wallpaper.london.heic │ │ │ ├── wallpaper.madrid.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── wallpaper.madrid.dark.heic │ │ │ │ └── wallpaper.madrid.heic │ │ │ ├── wallpaper.nyc.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── wallpaper.nyc.dark.heic │ │ │ │ └── wallpaper.nyc.heic │ │ │ ├── wallpaper.paris.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── wallpaper.paris.dark.heic │ │ │ │ └── wallpaper.paris.heic │ │ │ └── wallpaper.tokyo.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── wallpaper.tokyo.dark.heic │ │ │ │ └── wallpaper.tokyo.heic │ │ └── wave.colorset │ │ │ └── Contents.json │ ├── Constants.swift │ ├── Core │ │ ├── Analytics │ │ │ ├── Analytics.swift │ │ │ └── Screens.swift │ │ ├── AppState+Onboarding.swift │ │ ├── AppState+RemoteConfig.swift │ │ ├── AppState.swift │ │ ├── Auth │ │ │ └── AuthClient.swift │ │ ├── GlobalViewState.swift │ │ ├── Navigation │ │ │ ├── NavDestination.swift │ │ │ ├── NavigationState.swift │ │ │ └── PostDestination.swift │ │ ├── Networking │ │ │ ├── APIClient+Onboarding.swift │ │ │ ├── APIClient.swift │ │ │ ├── ImageUploadService.swift │ │ │ └── NetworkConnectionMonitor.swift │ │ ├── Permissions │ │ │ ├── ContactsPermissions.swift │ │ │ ├── LocationPermissions.swift │ │ │ ├── NotificationPermissions.swift │ │ │ └── PermissionManager.swift │ │ ├── Publishers │ │ │ ├── CommentPublisher.swift │ │ │ ├── PlacePublisher.swift │ │ │ ├── PostPublisher.swift │ │ │ └── UserPublisher.swift │ │ └── Types │ │ │ ├── Comment.swift │ │ │ ├── General.swift │ │ │ ├── Map.swift │ │ │ ├── Notifications.swift │ │ │ ├── Onboarding.swift │ │ │ ├── Place.swift │ │ │ ├── Post.swift │ │ │ └── User.swift │ ├── Helpers │ │ ├── Extensions │ │ │ ├── CLLocationCoordinate2D+Equatable.swift │ │ │ ├── MKCoordinateRegion+Equatable.swift │ │ │ ├── MKCoordinateRegion+Hashable.swift │ │ │ ├── MKCoordinateSpan+Equatable.swift │ │ │ ├── MKMapItem.swift │ │ │ ├── String+Extension.swift │ │ │ ├── String+Identifiable.swift │ │ │ ├── UIDevice+hasNotch.swift │ │ │ ├── UINavigationController+UIGestureRecognizerDelegate.swift │ │ │ ├── View+appear.swift │ │ │ ├── View+disappear.swift │ │ │ ├── View+modify.swift │ │ │ └── View+navigationBarColor.swift │ │ ├── HideKeyboard.swift │ │ ├── InvertBindings.swift │ │ ├── Navigator.swift │ │ └── NoButtonStyle.swift │ ├── Info.plist │ ├── Jimo.entitlements │ ├── JimoApp.swift │ ├── LaunchScreen.storyboard │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── Views │ │ ├── Auth │ │ ├── CreateProfileView.swift │ │ ├── EmailLogin.swift │ │ ├── EnterPhoneNumber.swift │ │ ├── HomeMenu.swift │ │ ├── LoggedInView.swift │ │ ├── RaisedButtonStyle.swift │ │ └── VerifyPhoneNumber.swift │ │ ├── Comment │ │ ├── CommentInputField.swift │ │ └── CommentItem.swift │ │ ├── Common │ │ ├── ActivityView.swift │ │ ├── FormInputText.swift │ │ ├── ImagePicker.swift │ │ ├── LargeButton.swift │ │ ├── NavTitle.swift │ │ ├── RefreshableScrollView.swift │ │ ├── SearchBar.swift │ │ ├── TextAlert.swift │ │ ├── Toast.swift │ │ └── URLImage.swift │ │ ├── ContentView.swift │ │ ├── DeepLinking │ │ ├── DeepLinkManager.swift │ │ ├── DeepLinkProfileLoadingScreen.swift │ │ └── DeepLinkViewPost.swift │ │ ├── Discover │ │ ├── DiscoverViewModel.swift │ │ ├── SearchViewModel.swift │ │ └── SuggestedUsersCarousel.swift │ │ ├── Feedback │ │ ├── Checkbox.swift │ │ ├── Feedback.swift │ │ └── RoundedButton.swift │ │ ├── Feeds │ │ ├── DeepLinkableFeedTab.swift │ │ ├── Feed.swift │ │ ├── FeedItem.swift │ │ └── FeedTabBody.swift │ │ ├── FirstOpenPopup.swift │ │ ├── Follow │ │ └── UserFollowFeed.swift │ │ ├── GuestAccount │ │ ├── AnonymousFeedPlaceholder.swift │ │ ├── AnonymousProfilePlaceholder.swift │ │ ├── MaybeGuestPostPage.swift │ │ ├── PostPagePlaceholder.swift │ │ └── SignUpTapSource.swift │ │ ├── MainAppView.swift │ │ ├── Map │ │ ├── BottomSheet │ │ │ ├── CategoryFilter.swift │ │ │ ├── CustomUserFilter.swift │ │ │ ├── MapBottomSheetBody.swift │ │ │ ├── MapBottomSheetHeader.swift │ │ │ ├── MapSearchField.swift │ │ │ ├── MapSearchResults.swift │ │ │ └── PlaceDetailsView.swift │ │ ├── Components │ │ │ ├── CircularCheckbox.swift │ │ │ ├── GlobalViewFilterButton.swift │ │ │ └── SignUpAlert.swift │ │ ├── Core │ │ │ ├── BlurBackground.swift │ │ │ ├── CurrentLocationButton.swift │ │ │ ├── LiteMapView.swift │ │ │ ├── MapSnapshotView.swift │ │ │ ├── MapTab.swift │ │ │ ├── MapViewV2.swift │ │ │ ├── RegionWrapper.swift │ │ │ └── UIKitMap │ │ │ │ ├── JimoMapView.swift │ │ │ │ ├── JimoPinView.swift │ │ │ │ └── MKJimoPinAnnotation.swift │ │ ├── MapType+Extension.swift │ │ ├── MapViewModel.swift │ │ ├── PostPage.swift │ │ └── Unauthed │ │ │ └── UnauthedMapUserFilter.swift │ │ ├── Notifications │ │ ├── NotificationFeed.swift │ │ └── NotificationFeedViewModel.swift │ │ ├── Onboarding │ │ ├── CityOnboarding.swift │ │ ├── CityPlaces.swift │ │ ├── Components │ │ │ ├── PhoneNumberTextFieldView.swift │ │ │ ├── SuggestedUserView.swift │ │ │ └── UserList.swift │ │ ├── FollowContacts.swift │ │ ├── FollowFeatured.swift │ │ ├── Models │ │ │ ├── ExistingContactStore.swift │ │ │ ├── FeaturedUserStore.swift │ │ │ ├── SelectedCity.swift │ │ │ └── SuggestedUserStore.swift │ │ ├── OnboardingView.swift │ │ ├── RequestLocation.swift │ │ ├── RequestPermission.swift │ │ └── UnauthedOnboarding.swift │ │ ├── Post │ │ ├── Create │ │ │ ├── CreatePost.swift │ │ │ ├── CreatePostCategoryPicker.swift │ │ │ ├── CreatePostStarPicker.swift │ │ │ ├── CreatePostVM.swift │ │ │ ├── ImageSelectionView.swift │ │ │ ├── LocationSearch.swift │ │ │ ├── MultilineTextField.swift │ │ │ └── PlaceSearch.swift │ │ ├── PostGridCell.swift │ │ └── View │ │ │ ├── PostComponents.swift │ │ │ ├── PostVM.swift │ │ │ ├── ViewPost.swift │ │ │ └── ViewPostCommentsViewModel.swift │ │ ├── Profile │ │ ├── DeactivatedProfileView.swift │ │ ├── Profile.swift │ │ ├── ProfileTab.swift │ │ └── ProfileViewModel.swift │ │ ├── Search │ │ ├── SearchTab.swift │ │ └── SearchUsers.swift │ │ ├── Settings │ │ ├── EditPreferences.swift │ │ ├── EditProfile.swift │ │ ├── NotificationSettings.swift │ │ └── Settings.swift │ │ ├── Share │ │ └── ShareButtonView.swift │ │ └── UITabView.swift ├── JimoTests │ ├── Info.plist │ └── JimoTests.swift └── JimoUITests │ ├── Info.plist │ └── JimoUITests.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # IDE files 141 | .idea/ 142 | 143 | # macOS 144 | .DS_Store 145 | 146 | # Private keys 147 | service-account-file.json 148 | 149 | -------------------------------------------------------------------------------- /Jimo/.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | -------------------------------------------------------------------------------- /Jimo/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | identifier_name: 2 | allowed_symbols: "_" 3 | excluded: 4 | - i 5 | - id 6 | - n 7 | - nc 8 | - s 9 | - q 10 | disabled_rules: 11 | - force_cast 12 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/App_store_1024_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/App_store_1024_1x.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "iPhone_Notifications_20_2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "iPhone_Notifications_20_3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "iPhone_Settings_29_2x.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "iPhone_Settings_29_3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "iPhone_Spotlight_40_2x.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "iPhone_Spotlight_40_3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "iPhone_App_60_2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "iPhone_App_60_3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "idiom" : "ipad", 53 | "scale" : "1x", 54 | "size" : "20x20" 55 | }, 56 | { 57 | "idiom" : "ipad", 58 | "scale" : "2x", 59 | "size" : "20x20" 60 | }, 61 | { 62 | "idiom" : "ipad", 63 | "scale" : "1x", 64 | "size" : "29x29" 65 | }, 66 | { 67 | "idiom" : "ipad", 68 | "scale" : "2x", 69 | "size" : "29x29" 70 | }, 71 | { 72 | "idiom" : "ipad", 73 | "scale" : "1x", 74 | "size" : "40x40" 75 | }, 76 | { 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "idiom" : "ipad", 83 | "scale" : "1x", 84 | "size" : "76x76" 85 | }, 86 | { 87 | "idiom" : "ipad", 88 | "scale" : "2x", 89 | "size" : "76x76" 90 | }, 91 | { 92 | "idiom" : "ipad", 93 | "scale" : "2x", 94 | "size" : "83.5x83.5" 95 | }, 96 | { 97 | "filename" : "App_store_1024_1x.png", 98 | "idiom" : "ios-marketing", 99 | "scale" : "1x", 100 | "size" : "1024x1024" 101 | } 102 | ], 103 | "info" : { 104 | "author" : "xcode", 105 | "version" : 1 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_2x.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_3x.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_2x.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_3x.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_2x.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_3x.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_2x.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_3x.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/background.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "light" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "luminosity", 37 | "value" : "dark" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "display-p3", 42 | "components" : { 43 | "alpha" : "1.000", 44 | "blue" : "0.000", 45 | "green" : "0.000", 46 | "red" : "0.000" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/activity.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "activity.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/activity.imageset/activity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/attraction.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "attraction.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/cafe.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cafe.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/cafe.imageset/cafe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/food.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "food.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/food.imageset/food.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/lodging.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "lodging.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/lodging.imageset/lodging.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/nightlife.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "nightlife.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/nightlife.imageset/nightlife.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/shopping.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "shopping.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Shopping bag.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Shopping bag-1.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/shopping.imageset/Shopping bag-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/categories/shopping.imageset/Shopping bag-1.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/shopping.imageset/Shopping bag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/categories/shopping.imageset/Shopping bag.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/categories/shopping.imageset/shopping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/categories/shopping.imageset/shopping.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/colors/activity.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x66", 9 | "green" : "0xBA", 10 | "red" : "0x65" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x48", 27 | "green" : "0xAC", 28 | "red" : "0x48" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/colors/attraction.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xC8", 9 | "green" : "0xC8", 10 | "red" : "0xC8" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/colors/cafe.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x7F", 9 | "green" : "0xB0", 10 | "red" : "0xE9" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x66", 27 | "green" : "0x97", 28 | "red" : "0xD0" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/colors/food.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x52", 9 | "green" : "0x69", 10 | "red" : "0xFE" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x3F", 27 | "green" : "0x53", 28 | "red" : "0xE3" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/colors/lightgray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xC8", 9 | "green" : "0xC8", 10 | "red" : "0xC8" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/colors/lodging.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xE5", 9 | "green" : "0x90", 10 | "red" : "0x34" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/colors/nightlife.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0x7F", 10 | "red" : "0xB4" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xF2", 27 | "green" : "0x62", 28 | "red" : "0x9E" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/colors/shopping.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xC0", 9 | "green" : "0x99", 10 | "red" : "0xFD" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xA5", 27 | "green" : "0x7D", 28 | "red" : "0xE2" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/colors/unselected.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "gray-gamma-22", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.844" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/foreground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "light" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.000", 27 | "green" : "0.000", 28 | "red" : "0.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "luminosity", 37 | "value" : "dark" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "srgb", 42 | "components" : { 43 | "alpha" : "1.000", 44 | "blue" : "1.000", 45 | "green" : "1.000", 46 | "red" : "1.000" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "icon@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/icon.imageset/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/icon.imageset/icon.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/icon.imageset/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/icon.imageset/icon@2x.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/icon.imageset/icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/icon.imageset/icon@3x.png -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/icon_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo_white.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "original" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/logo.imageset/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/permissions/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/permissions/contacts-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "contacts-icon.svg", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "contacts-icon-1.svg", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "preserves-vector-representation" : true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/permissions/contacts-icon.imageset/contacts-icon-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/permissions/contacts-icon.imageset/contacts-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/permissions/location-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "location-icon.svg", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "location-icon-1.svg", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "preserves-vector-representation" : true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/permissions/location-icon.imageset/location-icon-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/permissions/location-icon.imageset/location-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/permissions/next-button.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "74", 9 | "green" : "161", 10 | "red" : "74" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/permissions/notifications-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "notifications-icon.svg", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "notifications-icon-1.svg", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "preserves-vector-representation" : true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/secondary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "gray-gamma-22", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.900" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | }, 13 | { 14 | "appearances" : [ 15 | { 16 | "appearance" : "luminosity", 17 | "value" : "dark" 18 | } 19 | ], 20 | "color" : { 21 | "color-space" : "gray-gamma-22", 22 | "components" : { 23 | "alpha" : "1.000", 24 | "white" : "0.300" 25 | } 26 | }, 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/selectedContact.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "selectedContact.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/selectedContact.imageset/selectedContact.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/tabs/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/tabs/feedIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "feedIcon.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/tabs/feedIcon.imageset/feedIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/tabs/mapIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mapIcon.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/tabs/mapIcon.imageset/mapIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/tabs/postIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "postIcon.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/tabs/postIcon.imageset/postIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/tabs/profileIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "profileIcon.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/tabs/profileIcon.imageset/profileIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/tabs/searchIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "searchIcon.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/tabs/searchIcon.imageset/searchIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.bg.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "0.950" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.000", 27 | "green" : "0.000", 28 | "red" : "0.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.chicago.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "wallpaper.chicago.heic", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "wallpaper.chicago.dark.heic", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.chicago.imageset/wallpaper.chicago.dark.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.chicago.imageset/wallpaper.chicago.dark.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.chicago.imageset/wallpaper.chicago.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.chicago.imageset/wallpaper.chicago.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.la.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "wallpaper.la.heic", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "wallpaper.la.dark.heic", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.la.imageset/wallpaper.la.dark.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.la.imageset/wallpaper.la.dark.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.la.imageset/wallpaper.la.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.la.imageset/wallpaper.la.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.london.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "wallpaper.london.heic", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "wallpaper.london.dark.heic", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.london.imageset/wallpaper.london.dark.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.london.imageset/wallpaper.london.dark.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.london.imageset/wallpaper.london.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.london.imageset/wallpaper.london.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.madrid.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "wallpaper.madrid.heic", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "wallpaper.madrid.dark.heic", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.madrid.imageset/wallpaper.madrid.dark.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.madrid.imageset/wallpaper.madrid.dark.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.madrid.imageset/wallpaper.madrid.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.madrid.imageset/wallpaper.madrid.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.nyc.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "wallpaper.nyc.heic", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "wallpaper.nyc.dark.heic", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.nyc.imageset/wallpaper.nyc.dark.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.nyc.imageset/wallpaper.nyc.dark.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.nyc.imageset/wallpaper.nyc.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.nyc.imageset/wallpaper.nyc.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.paris.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "wallpaper.paris.heic", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "wallpaper.paris.dark.heic", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.paris.imageset/wallpaper.paris.dark.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.paris.imageset/wallpaper.paris.dark.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.paris.imageset/wallpaper.paris.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.paris.imageset/wallpaper.paris.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.tokyo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "wallpaper.tokyo.heic", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "wallpaper.tokyo.dark.heic", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.tokyo.imageset/wallpaper.tokyo.dark.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.tokyo.imageset/wallpaper.tokyo.dark.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.tokyo.imageset/wallpaper.tokyo.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/jimo-ios/c129f8b464964857ed9d2f1dac7d4b9b41b8705b/Jimo/Jimo/Assets.xcassets/wallpaper/wallpaper.tokyo.imageset/wallpaper.tokyo.heic -------------------------------------------------------------------------------- /Jimo/Jimo/Assets.xcassets/wave.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.941", 9 | "green" : "0.624", 10 | "red" : "0.282" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Jimo/Jimo/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 12/26/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Colors { 11 | static let colors = [ 12 | Color("food"), 13 | Color("cafe"), 14 | Color("lodging"), 15 | Color("activity"), 16 | Color("nightlife"), 17 | Color("shopping") 18 | ] 19 | 20 | static let gradientColors = Gradient(colors: colors) 21 | 22 | static let linearGradient = LinearGradient( 23 | gradient: gradientColors, startPoint: .leading, endPoint: .trailing) 24 | 25 | static let angularGradient = AngularGradient(colors: colors + [Color("food")], center: .center) 26 | } 27 | 28 | struct Category: Identifiable, Hashable { 29 | var id: String { key } 30 | var name: String 31 | var key: String 32 | var colorName: String { key } 33 | var imageName: String { key } 34 | } 35 | 36 | struct Categories { 37 | static let categories = [ 38 | Category(name: "Food", key: "food"), 39 | Category(name: "Cafe", key: "cafe"), 40 | Category(name: "Things to do", key: "activity"), 41 | Category(name: "Nightlife", key: "nightlife"), 42 | Category(name: "Lodging", key: "lodging"), 43 | Category(name: "Shopping", key: "shopping") 44 | ] 45 | } 46 | 47 | struct Stars { 48 | // Map instead of array so indexing is safe 49 | static let names = [ 50 | 0: "Not worth it", 51 | 1: "Worth a stop", 52 | 2: "Worth a detour", 53 | 3: "Worth a journey" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Analytics/Screens.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Screens.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/29/22. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Screen: String { 11 | // Signed out 12 | case landing 13 | case enterPhoneNumber 14 | case enterVerificationCode 15 | case createProfile 16 | case onboarding // TODO: Split into individual onboarding screens, fine for now 17 | case guestLocationOnboarding 18 | 19 | // Authenticated 20 | case mapTab 21 | case feedTab 22 | case forYouFeed 23 | case createPostSheet 24 | case searchTab 25 | case profileTab 26 | case savedPosts 27 | 28 | case searchUsers 29 | 30 | // Other views 31 | case enterLocationView 32 | 33 | case postView 34 | case profileView 35 | 36 | case settings 37 | case inviteContacts 38 | case notificationFeed 39 | 40 | case unknown 41 | } 42 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/AppState+Onboarding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState+Onboarding.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/11/23. 6 | // 7 | 8 | import Combine 9 | 10 | extension AppState { 11 | // MARK: - Onboarding routes 12 | func getOnboardingPlaces(for city: String) -> AnyPublisher { 13 | apiClient.getOnboardingPlaces(for: city) 14 | } 15 | 16 | func submitOnboardingPlaces( 17 | city: String?, 18 | posts: [MinimalCreatePostRequest], 19 | saves: [MinimalSavePlaceRequest] 20 | ) -> AnyPublisher { 21 | apiClient.submitOnboardingPlaces( 22 | OnboardingCreateMultiRequest( 23 | city: city, 24 | posts: posts, 25 | saves: saves 26 | ) 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/AppState+RemoteConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState+RemoteConfig.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/1/23. 6 | // 7 | 8 | import SwiftUI 9 | import FirebaseRemoteConfig 10 | 11 | extension AppState { 12 | func initializeRemoteConfig() { 13 | 14 | self.refreshRemoteConfig() 15 | } 16 | 17 | func refreshRemoteConfig() { 18 | Task { 19 | try await fetch() 20 | } 21 | } 22 | 23 | @MainActor 24 | private func fetch() async throws { 25 | let settings = RemoteConfigSettings() 26 | #if DEBUG 27 | settings.minimumFetchInterval = 0 28 | #endif 29 | RemoteConfig.remoteConfig().configSettings = settings 30 | do { 31 | try await RemoteConfig.remoteConfig().fetchAndActivate() 32 | } catch let error { 33 | print("Error fetching remote config \(error.localizedDescription)") 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/GlobalViewState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalViewState.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/12/21. 6 | // 7 | 8 | import Foundation 9 | 10 | class GlobalViewState: ObservableObject { 11 | @Published var showError = false 12 | @Published var errorMessage = "" 13 | 14 | @Published var showWarning = false 15 | @Published var warningMessage = "" 16 | 17 | @Published var showSuccess = false 18 | @Published var successMessage = "" 19 | 20 | @Published var showSignUpPage = false 21 | @Published var createPostPresented = false 22 | 23 | func setError(_ message: String) { 24 | self.errorMessage = message 25 | self.showError = true 26 | } 27 | 28 | func setWarning(_ message: String) { 29 | self.warningMessage = message 30 | self.showWarning = true 31 | } 32 | 33 | func setSuccess(_ message: String) { 34 | self.successMessage = message 35 | self.showSuccess = true 36 | } 37 | 38 | func showSignUpPage(_ type: SignUpTapSource) { 39 | Analytics.track(.guestAccountSignUpTap, parameters: ["source": type.analyticsSourceParameter]) 40 | self.showSignUpPage = true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Navigation/NavDestination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavDestination.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 5/20/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum NavDestination: Hashable { 11 | 12 | // Authentication 13 | case enterPhoneNumber 14 | case verifyPhoneNumber 15 | case emailLogin 16 | 17 | // Onboarding 18 | case cityOnboarding(city: String) 19 | 20 | // Main screens 21 | case profile(user: PublicUser) 22 | case post(post: Post, showSaveButton: Bool = false, highlightedComment: Comment? = nil) 23 | case notificationFeed 24 | 25 | // Map 26 | case liteMapView(post: Post) 27 | 28 | // Profile 29 | case followers(username: String) 30 | case following(username: String) 31 | 32 | // Settings 33 | case settings 34 | case editProfile 35 | case feedback 36 | case editPreferences 37 | 38 | // Deep linking 39 | case deepLink(entity: DeepLinkEntity) 40 | 41 | @ViewBuilder var view: some View { 42 | switch self { 43 | case .enterPhoneNumber: 44 | EnterPhoneNumber() 45 | case .verifyPhoneNumber: 46 | VerifyPhoneNumber(onVerify: {}) 47 | case .emailLogin: 48 | EmailLogin() 49 | case .cityOnboarding(let city): 50 | CityPlaces(city: city) 51 | case .liteMapView(let post): 52 | LiteMapView(post: post) 53 | case .profile(let user): 54 | ProfileScreen(initialUser: user) 55 | case .post(let post, let showSave, let comment): 56 | ViewPost(initialPost: post, highlightedComment: comment, showSaveButton: showSave) 57 | case .notificationFeed: 58 | NotificationFeed() 59 | case .settings: 60 | Settings() 61 | case .editProfile: 62 | EditProfile() 63 | case .editPreferences: 64 | EditPreferences() 65 | case .feedback: 66 | Feedback() 67 | case .followers(let username): 68 | FollowFeed(navTitle: "Followers", type: .followers, username: username) 69 | case .following(let username): 70 | FollowFeed(navTitle: "Following", type: .following, username: username) 71 | case .deepLink(let entity): 72 | entity.view() 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Navigation/NavigationState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationState.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 5/20/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class NavigationState: ObservableObject { 11 | @Published var path: [NavDestination] = [] 12 | 13 | func push(_ dest: NavDestination) { 14 | path.append(dest) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Navigation/PostDestination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostDestination.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 4/10/22. 6 | // 7 | 8 | import Foundation 9 | 10 | enum PostDestination: Identifiable { 11 | case profile(PublicUser) 12 | case post(PostId) 13 | case map(PostId) 14 | 15 | var id: String { 16 | switch self { 17 | case .profile(let user): 18 | return "profile:\(user.id)" 19 | case .post(let postId): 20 | return "post:\(postId)" 21 | case .map(let postId): 22 | return "map:\(postId)" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Networking/APIClient+Onboarding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient+Onboarding.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/10/23. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | extension Endpoint { 12 | static func onboardingPlaces(city: String? = nil) -> Endpoint { 13 | .init( 14 | path: "/onboarding/places", 15 | queryItems: city != nil ? [URLQueryItem(name: "city", value: city!)] : [] 16 | ) 17 | } 18 | } 19 | 20 | extension APIClient { 21 | func getOnboardingPlaces(for city: String) -> AnyPublisher { 22 | doRequest(endpoint: .onboardingPlaces(city: city)) 23 | } 24 | 25 | func submitOnboardingPlaces(_ request: OnboardingCreateMultiRequest) -> AnyPublisher { 26 | doRequest(endpoint: .onboardingPlaces(), httpMethod: "POST", body: request) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Networking/ImageUploadService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageUploadService.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/23/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | class ImageUploadService { 12 | 13 | /// Upload the image to the given URL with content type `image/jpeg`, field name `file`, and file name `upload.jpg`. 14 | static func uploadImage(url: URL, imageData: Data, token: String) -> URLSession.DataTaskPublisher { 15 | let boundary = "Boundary-\(UUID().uuidString)" 16 | var body = Data() 17 | var request = URLRequest(url: url) 18 | request.httpMethod = "POST" 19 | request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") 20 | request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") 21 | body.append(toData("--\(boundary)\r\n")) 22 | body.append(toData("Content-Disposition: form-data; name=\"file\"; filename=\"upload.jpg\"\r\n")) 23 | body.append(toData("Content-Type: image/jpeg\r\n\r\n")) 24 | body.append(imageData) 25 | body.append(toData("\r\n--\(boundary)--")) 26 | request.httpBody = body 27 | return URLSession.shared.dataTaskPublisher(for: request) 28 | } 29 | } 30 | 31 | private func toData(_ s: String) -> Data { 32 | s.data(using: .utf8)! 33 | } 34 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Networking/NetworkConnectionMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkConnectionMonitor.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/31/22. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | import Network 11 | 12 | class NetworkConnectionMonitor: ObservableObject { 13 | let monitor = NWPathMonitor() 14 | var cancelBag: Set = Set() 15 | 16 | @Published var connected = true 17 | 18 | func listen() { 19 | monitor.pathUpdateHandler = { [weak self] path in 20 | DispatchQueue.main.async { 21 | guard let self = self else { 22 | return 23 | } 24 | if path.status != .satisfied { 25 | self.connected = false 26 | } else { 27 | self.connected = true 28 | } 29 | } 30 | } 31 | monitor.start(queue: DispatchQueue(label: "NetworkMonitor")) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Permissions/ContactsPermissions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactsPermissions.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/28/22. 6 | // 7 | 8 | import SwiftUI 9 | import Contacts 10 | 11 | extension PermissionManager { 12 | func requestContacts(_ callback: @escaping (Bool, Error?) -> Void) { 13 | if contactsAuthStatus() == .denied { 14 | UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, completionHandler: { (_) in }) 15 | } 16 | contactStore.requestAccess(for: .contacts) { (success, error) in 17 | Analytics.track(success ? .contactsPermissionsAllowed : .contactsPermissionsDenied) 18 | callback(success, error) 19 | } 20 | } 21 | 22 | func contactsAuthStatus() -> PermissionStatus { 23 | switch CNContactStore.authorizationStatus(for: .contacts) { 24 | case .notDetermined: 25 | return .notRequested 26 | case .authorized: 27 | return .authorized 28 | case .restricted: 29 | return .authorized 30 | case .denied: 31 | return .denied 32 | @unknown default: 33 | return .notRequested 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Permissions/LocationPermissions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Location.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/27/22. 6 | // 7 | 8 | import SwiftUI 9 | import MapKit 10 | 11 | extension PermissionManager: CLLocationManagerDelegate { 12 | func requestLocation() { 13 | self.locationManager.delegate = self 14 | if self.locationManager.authorizationStatus == .denied { 15 | UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, completionHandler: { (_) in }) 16 | } 17 | self.locationManager.requestWhenInUseAuthorization() 18 | self.locationManager.startUpdatingLocation() 19 | } 20 | 21 | func getLocation() -> CLLocation? { 22 | return self.locationManager.location 23 | } 24 | 25 | func hasRequestedLocation() -> Bool { 26 | return self.locationManager.authorizationStatus != .notDetermined 27 | } 28 | 29 | func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { 30 | if manager.authorizationStatus == .denied { 31 | Analytics.track(.locationPermissionsDenied) 32 | } else if manager.authorizationStatus == .authorizedWhenInUse { 33 | Analytics.track(.locationPermissionsAllowed) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Permissions/NotificationPermissions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notifications.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/27/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension PermissionManager { 11 | func requestNotifications() { 12 | UNUserNotificationCenter.current().getNotificationSettings { settings in 13 | if settings.authorizationStatus == .denied { 14 | DispatchQueue.main.async { 15 | UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, completionHandler: { (_) in }) 16 | } 17 | } else if settings.authorizationStatus != .authorized { 18 | UNUserNotificationCenter.current().requestAuthorization( 19 | options: [.alert, .badge, .sound], 20 | completionHandler: { (success, _) in 21 | Analytics.track(success ? .notificationPermissionsAllowed : .notificationPermissionsDenied) 22 | }) 23 | } 24 | } 25 | } 26 | 27 | func getNotificationAuthStatus(callback: @escaping (UNAuthorizationStatus) -> Void) { 28 | UNUserNotificationCenter.current().getNotificationSettings { settings in 29 | callback(settings.authorizationStatus) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Permissions/PermissionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermissionManager.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/27/22. 6 | // 7 | 8 | import SwiftUI 9 | import Contacts 10 | import MapKit 11 | 12 | enum PermissionStatus { 13 | case notRequested, authorized, denied 14 | } 15 | 16 | class PermissionManager: NSObject { 17 | static let shared = PermissionManager() 18 | 19 | let locationManager = CLLocationManager() 20 | let contactStore = CNContactStore() 21 | } 22 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Publishers/CommentPublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentPublisher.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 7/25/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CommentLikePayload { 11 | var commentId: CommentId 12 | var likeCount: Int 13 | var liked: Bool 14 | } 15 | 16 | class CommentPublisher { 17 | let notificationCenter = NotificationCenter.default 18 | 19 | static let commentCreated = Notification.Name("comment:created") 20 | static let commentLikes = Notification.Name("comment:likes") 21 | static let commentDeleted = Notification.Name("comment:deleted") 22 | 23 | func commentCreated(comment: Comment) { 24 | notificationCenter.post(name: CommentPublisher.commentCreated, object: comment) 25 | } 26 | 27 | func commentLikes(commentId: CommentId, likeCount: Int, liked: Bool) { 28 | notificationCenter.post(name: CommentPublisher.commentLikes, object: CommentLikePayload(commentId: commentId, likeCount: likeCount, liked: liked)) 29 | } 30 | 31 | func commentDeleted(commentId: CommentId) { 32 | notificationCenter.post(name: CommentPublisher.commentDeleted, object: commentId) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Publishers/PlacePublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlacePublisher.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 1/24/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct PlaceSavePayload: Codable, Equatable { 11 | var placeId: PlaceId 12 | var save: SavedPlace? 13 | var createPlaceRequest: MaybeCreatePlaceRequest? 14 | } 15 | 16 | class PlacePublisher { 17 | let notificationCenter = NotificationCenter.default 18 | 19 | static let placeSaved = Notification.Name("place:saved") 20 | 21 | func placeSaved(_ payload: PlaceSavePayload) { 22 | notificationCenter.post(name: PlacePublisher.placeSaved, object: payload) 23 | } 24 | 25 | func placeUnsaved(_ placeId: PlaceId) { 26 | notificationCenter.post(name: PlacePublisher.placeSaved, object: PlaceSavePayload(placeId: placeId)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Publishers/PostPublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostPublisher.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 7/19/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct PostLikePayload { 11 | var postId: PostId 12 | var likeCount: Int 13 | var liked: Bool 14 | } 15 | 16 | class PostPublisher { 17 | let notificationCenter = NotificationCenter.default 18 | 19 | static let postCreated = Notification.Name("post:created") 20 | static let postUpdated = Notification.Name("post:updated") 21 | static let postLiked = Notification.Name("post:liked") 22 | static let postDeleted = Notification.Name("post:deleted") 23 | 24 | func postCreated(post: Post) { 25 | notificationCenter.post(name: PostPublisher.postCreated, object: post) 26 | } 27 | 28 | func postUpdated(post: Post) { 29 | notificationCenter.post(name: PostPublisher.postUpdated, object: post) 30 | } 31 | 32 | func postLiked(postId: PostId, likeCount: Int) { 33 | notificationCenter.post( 34 | name: PostPublisher.postLiked, 35 | object: PostLikePayload(postId: postId, likeCount: likeCount, liked: true) 36 | ) 37 | } 38 | 39 | func postUnliked(postId: PostId, likeCount: Int) { 40 | notificationCenter.post( 41 | name: PostPublisher.postLiked, 42 | object: PostLikePayload(postId: postId, likeCount: likeCount, liked: false) 43 | ) 44 | } 45 | 46 | func postDeleted(postId: PostId) { 47 | notificationCenter.post(name: PostPublisher.postDeleted, object: postId) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Publishers/UserPublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserPublisher.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 7/27/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UserRelationPayload { 11 | var username: String 12 | var relation: UserRelation? 13 | } 14 | 15 | class UserPublisher { 16 | let notificationCenter = NotificationCenter.default 17 | 18 | static let userRelationChanged = Notification.Name("user:relation") 19 | 20 | func userRelationChanged(username: String, relation: UserRelation?) { 21 | notificationCenter.post(name: UserPublisher.userRelationChanged, object: UserRelationPayload(username: username, relation: relation)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Types/Comment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Comment.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 5/8/21. 6 | // 7 | 8 | import Foundation 9 | 10 | typealias CommentId = String 11 | 12 | struct Comment: Codable, Hashable, Equatable, Identifiable { 13 | var id: CommentId { 14 | commentId 15 | } 16 | var commentId: CommentId 17 | var user: PublicUser 18 | var postId: PostId 19 | var content: String 20 | var createdAt: Date 21 | var likeCount: Int 22 | var liked: Bool 23 | } 24 | 25 | struct CommentPage: Codable { 26 | var comments: [Comment] 27 | var cursor: String? 28 | } 29 | 30 | struct CreateCommentRequest: Codable { 31 | var postId: PostId 32 | var content: String 33 | } 34 | 35 | struct LikeCommentResponse: Codable { 36 | var likes: Int 37 | } 38 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Types/General.swift: -------------------------------------------------------------------------------- 1 | // 2 | // General.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 11/24/20. 6 | // 7 | 8 | import Foundation 9 | 10 | typealias OnFinish = () -> Void 11 | typealias OnRefresh = (@escaping OnFinish) -> Void 12 | typealias OnLoadMore = () -> Void 13 | 14 | struct FirebaseUser { 15 | var uid: String 16 | var phoneNumber: String? 17 | } 18 | 19 | struct NotificationTokenRequest: Codable { 20 | var token: String 21 | } 22 | 23 | struct FeedbackRequest: Codable { 24 | var contents: String 25 | var followUp: Bool 26 | } 27 | 28 | struct SimpleResponse: Codable { 29 | var success: Bool 30 | } 31 | 32 | typealias ImageId = String 33 | struct ImageUploadResponse: Codable { 34 | var imageId: ImageId 35 | } 36 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Types/Map.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapV3.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/20/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct RectangularRegion: Codable, Equatable, Hashable { 11 | var xMin: Double 12 | var yMin: Double 13 | var xMax: Double 14 | var yMax: Double 15 | } 16 | 17 | enum MapType: String, Codable { 18 | case community, following, saved, custom, me 19 | } 20 | 21 | struct GetMapRequest: Codable, Equatable { 22 | var region: RectangularRegion 23 | var categories: [String] = [] 24 | var mapType: MapType 25 | var userIds: [String] = [] 26 | } 27 | 28 | struct MapPin: Identifiable, Codable, Equatable { 29 | var id: String { 30 | placeId 31 | } 32 | var placeId: String 33 | var location: Location 34 | var icon: MapPinIcon 35 | } 36 | 37 | struct MapPinIcon: Codable, Equatable { 38 | var category: String? 39 | var iconUrl: String? 40 | var numPosts: Int 41 | } 42 | 43 | struct MapResponse: Codable, Equatable { 44 | var pins: [MapPin] 45 | } 46 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Types/Notifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notifications.swift 3 | // Jimo 4 | // 5 | // Created by Jeff Rohlman on 3/2/21. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ItemType: String, Codable { 11 | case follow 12 | case like 13 | case comment 14 | case save 15 | case unknown 16 | } 17 | 18 | extension ItemType { 19 | public init(from decoder: Decoder) throws { 20 | self = try ItemType(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown 21 | } 22 | } 23 | 24 | struct NotificationItem: Identifiable, Codable, Hashable { 25 | var id: String { 26 | itemId 27 | } 28 | var type: ItemType 29 | var createdAt: Date 30 | var user: PublicUser 31 | var itemId: String 32 | var post: Post? 33 | var comment: Comment? 34 | } 35 | 36 | struct NotificationFeedResponse: Codable { 37 | var notifications: [NotificationItem] 38 | var cursor: String? 39 | } 40 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Types/Onboarding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Onboarding.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct PlaceTilePage: Codable, Equatable, Hashable { 11 | var places: [PlaceTile] 12 | } 13 | 14 | struct PlaceTile: Codable, Equatable, Hashable { 15 | var placeId: PlaceId 16 | var name: String 17 | var imageUrl: String 18 | var category: String 19 | var description: String 20 | } 21 | 22 | struct MinimalSavePlaceRequest: Codable, Equatable, Hashable { 23 | var placeId: PlaceId 24 | } 25 | 26 | struct MinimalCreatePostRequest: Codable, Equatable, Hashable { 27 | var placeId: PlaceId 28 | var category: String 29 | var stars: Int? 30 | } 31 | 32 | struct OnboardingCreateMultiRequest: Codable, Equatable, Hashable { 33 | var city: String? 34 | var posts: [MinimalCreatePostRequest] 35 | var saves: [MinimalSavePlaceRequest] 36 | } 37 | -------------------------------------------------------------------------------- /Jimo/Jimo/Core/Types/Post.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Post.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 11/6/20. 6 | // 7 | 8 | import Foundation 9 | import MapKit 10 | 11 | typealias PostId = String 12 | 13 | struct Post: Codable, Equatable, Identifiable, Hashable { 14 | var id: PostId { 15 | postId 16 | } 17 | var postId: PostId 18 | var user: PublicUser 19 | var place: Place 20 | var category: String 21 | var content: String 22 | var stars: Int? 23 | var media: [PostMediaItem]? 24 | var createdAt: Date 25 | var likeCount: Int 26 | var commentCount: Int 27 | var liked: Bool 28 | var saved: Bool 29 | 30 | var imageUrl: String? { 31 | media?.first?.url 32 | } 33 | 34 | var location: CLLocationCoordinate2D { 35 | place.location.coordinate() 36 | } 37 | 38 | var postUrl: URL { 39 | URL(string: "https://go.jimoapp.com/view-post?id=\(id)")! 40 | } 41 | } 42 | 43 | struct PostMediaItem: Codable, Equatable, Identifiable, Hashable { 44 | var id: String 45 | var blobName: String? 46 | var url: String 47 | } 48 | 49 | struct FeedResponse: Codable { 50 | var posts: [Post] 51 | var cursor: String? 52 | } 53 | 54 | struct CreatePostRequest: Codable { 55 | /// One of placeId and place must be specified 56 | var placeId: PlaceId? 57 | var place: MaybeCreatePlaceRequest? 58 | var category: String 59 | var content: String 60 | var stars: Int? 61 | var media: [String] 62 | } 63 | 64 | struct DeletePostResponse: Codable { 65 | var deleted: Bool 66 | } 67 | 68 | struct LikePostResponse: Codable { 69 | var likes: Int 70 | } 71 | 72 | struct ReportPostRequest: Codable { 73 | var details: String? 74 | } 75 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Extensions/CLLocationCoordinate2D+Equatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLLocationCoordinate2D+Equatable.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/6/21. 6 | // 7 | 8 | import Foundation 9 | import MapKit 10 | 11 | extension CLLocationCoordinate2D: Equatable { 12 | public static func ==(lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { 13 | return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Extensions/MKCoordinateRegion+Equatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MKCoordinateRegion+Equatable.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/6/21. 6 | // 7 | 8 | import Foundation 9 | import MapKit 10 | 11 | extension MKCoordinateRegion: Equatable { 12 | public static func ==(lhs: MKCoordinateRegion, rhs: MKCoordinateRegion) -> Bool { 13 | return lhs.center == rhs.center && lhs.span == rhs.span 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Extensions/MKCoordinateRegion+Hashable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MKCoordinateRegion+Hashable.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/30/22. 6 | // 7 | 8 | import Foundation 9 | import MapKit 10 | 11 | extension MKCoordinateRegion: Hashable { 12 | public func hash(into hasher: inout Hasher) { 13 | hasher.combine(center.latitude) 14 | hasher.combine(center.longitude) 15 | hasher.combine(span.latitudeDelta) 16 | hasher.combine(span.longitudeDelta) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Extensions/MKCoordinateSpan+Equatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MKCoordinateSpan+Equatable.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/6/21. 6 | // 7 | 8 | import Foundation 9 | import MapKit 10 | 11 | extension MKCoordinateSpan: Equatable { 12 | public static func ==(lhs: MKCoordinateSpan, rhs: MKCoordinateSpan) -> Bool { 13 | return lhs.latitudeDelta == rhs.latitudeDelta && lhs.longitudeDelta == rhs.longitudeDelta 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Extensions/MKMapItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MKMapItem.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 1/19/23. 6 | // 7 | 8 | import MapKit 9 | 10 | extension MKMapItem { 11 | var circularRegion: Region? { 12 | if let area = self.placemark.region as? CLCircularRegion { 13 | return Region(coord: area.center, radius: area.radius.magnitude) 14 | } 15 | return nil 16 | } 17 | 18 | var maybeCreatePlaceRequest: MaybeCreatePlaceRequest? { 19 | guard let name = self.name else { 20 | return nil 21 | } 22 | return MaybeCreatePlaceRequest( 23 | name: name, 24 | location: Location(coord: self.placemark.coordinate), 25 | region: self.circularRegion, 26 | additionalData: AdditionalPlaceDataRequest(self) 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Extensions/String+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extension.swift 3 | // Community - Find Your People 4 | // 5 | // Created by Xilin Liu on 1/31/20. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | /// Given self as the singular form and quantity as the number, returns a pluralized version of the string 12 | /// "apple".plural(1) -> "1 apple" 13 | /// "orange".plural(2) -> "2 oranges" 14 | func plural(_ quantity: Int) -> String { 15 | if quantity == 1 { 16 | // singular 17 | return "\(quantity) \(self)" 18 | } else { 19 | // special cases 20 | // people 21 | if self == "person" { 22 | return "\(quantity) people" 23 | } 24 | 25 | // plural 26 | return "\(quantity) \(self)s" 27 | } 28 | } 29 | 30 | /// converts text to `snake_case` 31 | var snakeized: String { 32 | let pattern = "([a-z0-9])([A-Z])" 33 | let regex = try? NSRegularExpression(pattern: pattern, options: []) 34 | let range = NSRange(location: 0, length: count) 35 | return regex?.stringByReplacingMatches( 36 | in: self, options: [], range: range, withTemplate: "$1_$2").lowercased() ?? "" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Extensions/String+Identifiable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Identifiable.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 4/10/22. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String: Identifiable { 11 | public var id: String { 12 | self 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Extensions/UIDevice+hasNotch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice+hasNotch.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 9/15/21. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIDevice { 11 | var hasNotch: Bool { 12 | if #available(iOS 11.0, tvOS 11.0, *) { 13 | return UIApplication.shared.delegate?.window??.safeAreaInsets.top ?? 0 > 20 14 | } 15 | return false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Extensions/UINavigationController+UIGestureRecognizerDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+UIGestureRecognizerDelegate.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/5/21. 6 | // 7 | 8 | import UIKit 9 | 10 | // This allows us to swipe back even when the navigation bar is hidden 11 | extension UINavigationController: UIGestureRecognizerDelegate { 12 | override open func viewDidLoad() { 13 | super.viewDidLoad() 14 | interactivePopGestureRecognizer?.delegate = self 15 | } 16 | 17 | public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 18 | return viewControllers.count > 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Extensions/View+appear.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+appear.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/27/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // onAppear and onDisappear are buggy, this is a more stable way of handling onAppear and onDisappear events. 11 | struct UIKitAppear: UIViewControllerRepresentable { 12 | let action: () -> Void 13 | 14 | func makeUIViewController(context: Context) -> UIAppearViewController { 15 | let vc = UIAppearViewController() 16 | vc.action = action 17 | return vc 18 | } 19 | 20 | func updateUIViewController(_ controller: UIAppearViewController, context: Context) { 21 | } 22 | } 23 | 24 | class UIAppearViewController: UIViewController { 25 | var action: (() -> Void)? 26 | 27 | override func viewDidLoad() { 28 | view.addSubview(UILabel()) 29 | } 30 | 31 | override func viewDidAppear(_ animated: Bool) { 32 | action?() 33 | } 34 | } 35 | 36 | extension View { 37 | func appear(_ perform: @escaping () -> Void) -> some View { 38 | self.background(UIKitAppear(action: perform)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Extensions/View+disappear.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+disappear.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/27/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // onAppear and onDisappear are buggy, this is a more stable way of handling onAppear and onDisappear events. 11 | struct UIKitDisappear: UIViewControllerRepresentable { 12 | let action: () -> Void 13 | 14 | func makeUIViewController(context: Context) -> UIDisappearViewController { 15 | let vc = UIDisappearViewController() 16 | vc.action = action 17 | return vc 18 | } 19 | 20 | func updateUIViewController(_ controller: UIDisappearViewController, context: Context) { 21 | } 22 | } 23 | 24 | class UIDisappearViewController: UIViewController { 25 | var action: (() -> Void)? 26 | 27 | override func viewDidLoad() { 28 | view.addSubview(UILabel()) 29 | } 30 | 31 | override func viewDidDisappear(_ animated: Bool) { 32 | action?() 33 | } 34 | } 35 | 36 | extension View { 37 | func disappear(_ perform: @escaping () -> Void) -> some View { 38 | self.background(UIKitDisappear(action: perform)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Extensions/View+modify.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+modify.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 1/30/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | @ViewBuilder 12 | func modify(@ViewBuilder _ transform: (Self) -> Content?) -> some View { 13 | if let view = transform(self), !(view is EmptyView) { 14 | view 15 | } else { 16 | self 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Extensions/View+navigationBarColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationViewModifier.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 11/7/20. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // From https://filipmolcik.com/navigationview-dynamic-background-color-in-swiftui/ 12 | struct NavigationBarModifier: ViewModifier { 13 | var backgroundColor: UIColor? 14 | 15 | init(backgroundColor: UIColor?) { 16 | self.backgroundColor = backgroundColor 17 | let coloredAppearance = UINavigationBarAppearance() 18 | coloredAppearance.configureWithTransparentBackground() 19 | coloredAppearance.backgroundColor = .clear 20 | UINavigationBar.appearance().standardAppearance = coloredAppearance 21 | UINavigationBar.appearance().compactAppearance = coloredAppearance 22 | UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance 23 | // UINavigationBar.appearance().tintColor = .white 24 | } 25 | 26 | func body(content: Content) -> some View { 27 | ZStack { 28 | content 29 | VStack { 30 | GeometryReader { geometry in 31 | Color(self.backgroundColor ?? .clear) 32 | .frame(height: geometry.safeAreaInsets.top) 33 | .edgesIgnoringSafeArea(.top) 34 | Spacer() 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | extension View { 42 | func navigationBarColor(_ backgroundColor: UIColor?) -> some View { 43 | self.modifier(NavigationBarModifier(backgroundColor: backgroundColor)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/HideKeyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HideKeyboard.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 11/14/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | func hideKeyboard() { 11 | UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) 12 | } 13 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/InvertBindings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InvertBindings.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/27/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | prefix func ! (value: Binding) -> Binding { 11 | Binding( 12 | get: { !value.wrappedValue }, 13 | set: { value.wrappedValue = !$0 } 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/Navigator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Navigator.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 1/30/23. 6 | // 7 | 8 | import SwiftUI 9 | import NavigationBackport 10 | 11 | struct Navigator: View where Content: View { 12 | @ObservedObject var state: NavigationState 13 | var content: () -> Content 14 | 15 | var body: some View { 16 | NBNavigationStack(path: $state.path) { 17 | content() 18 | .nbNavigationDestination(for: NavDestination.self, destination: \.view) 19 | }.environmentObject(state) 20 | } 21 | } 22 | 23 | // Useful for easier styling using toolbars and navigation title 24 | struct FakeNavigator: View where Content: View { 25 | var content: () -> Content 26 | 27 | var body: some View { 28 | NBNavigationStack(root: content) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Jimo/Jimo/Helpers/NoButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoButtonStyle.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/25/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NoButtonStyle: ButtonStyle { 11 | func makeBody(configuration: Configuration) -> some View { 12 | configuration.label 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Jimo/Jimo/Jimo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.associated-domains 8 | 9 | applinks:go.jimoapp.com 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Jimo/Jimo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Auth/EmailLogin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmailLogin.swift 3 | // Jimo 4 | // 5 | // Created by Kevin Nizza on 4/28/22. 6 | // 7 | 8 | import SwiftUI 9 | import PopupView 10 | import Combine 11 | 12 | struct EmailLogin: View { 13 | @EnvironmentObject var appState: AppState 14 | @StateObject var viewModel = ViewModel() 15 | 16 | func signIn() { 17 | hideKeyboard() 18 | viewModel.signIn(appState: appState) 19 | } 20 | 21 | var body: some View { 22 | ZStack { 23 | VStack(spacing: 20) { 24 | Image("logo") 25 | .aspectRatio(contentMode: .fit) 26 | 27 | Text("Welcome back!") 28 | TextField("Email", text: $viewModel.email) 29 | .padding(12) 30 | .background(RoundedRectangle(cornerRadius: 10) 31 | .stroke(Color("foreground"))) 32 | 33 | SecureField("Password", text: $viewModel.password) 34 | .padding(12) 35 | .background(RoundedRectangle(cornerRadius: 10) 36 | .stroke(Color("foreground"))) 37 | 38 | Button(action: signIn) { 39 | Text("Sign in") 40 | .frame(minWidth: 0, maxWidth: .infinity) 41 | .frame(height: 50) 42 | .foregroundColor(.white) 43 | .background(Color("shopping")) 44 | .cornerRadius(10) 45 | } 46 | .shadow(radius: 5) 47 | 48 | } 49 | .padding(.horizontal, 24) 50 | } 51 | .popup(isPresented: $viewModel.showError) { 52 | Toast(text: viewModel.error, type: .error) 53 | } customize: { 54 | $0.type(.toast).position(.bottom).autohideIn(2) 55 | } 56 | .navigationBarTitleDisplayMode(.inline) 57 | .navigationTitle(Text("Super secret menu")) 58 | } 59 | } 60 | 61 | extension EmailLogin { 62 | class ViewModel: ObservableObject { 63 | var cancellable: Cancellable? 64 | 65 | @Published var email = "" 66 | @Published var password = "" 67 | 68 | @Published var error = "" 69 | @Published var showError = false 70 | 71 | func setError(_ error: String) { 72 | showError = true 73 | self.error = error 74 | } 75 | 76 | func signIn(appState: AppState) { 77 | cancellable = appState.signIn(email: email, password: password) 78 | .sink { completion in 79 | if case let .failure(error) = completion { 80 | print("Error while signing in", error) 81 | self.setError(error.localizedDescription) 82 | } 83 | } receiveValue: { _ in 84 | 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Auth/LoggedInView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoggedInView.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/14/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LoggedInView: View { 11 | @EnvironmentObject var appState: AppState 12 | @EnvironmentObject var globalViewState: GlobalViewState 13 | @ObservedObject var onboardingModel: OnboardingModel 14 | 15 | let currentUser: PublicUser 16 | 17 | var body: some View { 18 | if onboardingModel.isUserOnboarded { 19 | MainAppView(notificationsModel: appState.notificationsModel, currentUser: currentUser) 20 | } else { 21 | OnboardingView().environmentObject(appState.onboardingModel) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Auth/RaisedButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RaisedButtonStyle.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 7/26/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RaisedButtonStyle: ButtonStyle { 11 | func makeBody(configuration: Configuration) -> some View { 12 | configuration.label 13 | .shadow(radius: 4, x: 0.0, y: configuration.isPressed ? 0 : 4) 14 | .offset(y: configuration.isPressed ? 4 : 0) 15 | .animation(.easeIn(duration: 0.1), value: configuration.isPressed) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Comment/CommentInputField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentInputField.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 5/8/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CommentInputField: View { 11 | @Binding var text: String 12 | var isFocused: FocusState.Binding 13 | var submitting: Bool 14 | 15 | var buttonColor: Color = .black 16 | 17 | var onSubmit: () -> Void 18 | 19 | var inputBody: some View { 20 | TextField("Add a comment", text: $text, onCommit: onSubmit) 21 | .focused(isFocused) 22 | .disabled(submitting) 23 | .submitLabel(.send) 24 | } 25 | 26 | var body: some View { 27 | HStack(spacing: 0) { 28 | HStack { 29 | inputBody 30 | if isFocused.wrappedValue { 31 | Button("Cancel") { 32 | DispatchQueue.main.async { 33 | isFocused.wrappedValue = false 34 | } 35 | }.foregroundColor(.blue) 36 | } 37 | } 38 | .font(.system(size: 12)) 39 | .padding(.horizontal, 5) 40 | .padding(.vertical, 5) 41 | .padding(.horizontal, 6) 42 | .padding(.vertical, 6) 43 | .background(Color("foreground").opacity(0.2)) 44 | .cornerRadius(10) 45 | 46 | Group { 47 | if text.count > 0 { 48 | Spacer() 49 | 50 | Button(action: { 51 | withAnimation { 52 | DispatchQueue.main.async { 53 | isFocused.wrappedValue = false 54 | } 55 | onSubmit() 56 | } 57 | }) { 58 | Image(systemName: "paperplane.fill") 59 | .foregroundColor(buttonColor) 60 | } 61 | .padding(.trailing, 10) 62 | .transition(.move(edge: .trailing)) 63 | } 64 | } 65 | } 66 | .animation(.easeInOut, value: text.count) 67 | .padding(8) 68 | .background(Color("background")) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Common/ActivityView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityView.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 5/5/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ActivityView: UIViewControllerRepresentable { 11 | let shareAction: ShareAction 12 | var isPresented: Binding 13 | 14 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { 15 | Analytics.track(shareAction.presentedEvent) 16 | let controller = UIActivityViewController(activityItems: [ActivityItemSource(shareAction)], applicationActivities: nil) 17 | controller.completionWithItemsHandler = { (activityType, completed, _, _) in 18 | if completed { 19 | Analytics.track(self.shareAction.completedEvent, parameters: ["activity_type": activityType?.rawValue]) 20 | } else { 21 | Analytics.track(self.shareAction.cancelledEvent) 22 | } 23 | self.isPresented.wrappedValue = false 24 | } 25 | return controller 26 | } 27 | 28 | func updateUIViewController( 29 | _ uiViewController: UIActivityViewController, 30 | context: UIViewControllerRepresentableContext 31 | ) { 32 | } 33 | } 34 | 35 | private class ActivityItemSource: NSObject, UIActivityItemSource { 36 | let shareAction: ShareAction 37 | 38 | var shareTitle: String { 39 | "Check \(shareAction.name) out on Jimo" 40 | } 41 | 42 | init(_ shareAction: ShareAction) { 43 | self.shareAction = shareAction 44 | } 45 | 46 | func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { 47 | return shareTitle 48 | } 49 | 50 | func activityViewController( 51 | _ activityViewController: UIActivityViewController, 52 | itemForActivityType activityType: UIActivity.ActivityType? 53 | ) -> Any? { 54 | if activityType == .message { 55 | /// iMessage is stripping the query params from the URL for some reason 56 | /// https://developer.apple.com/forums/thread/131930 57 | return "\(shareTitle)\n\(shareAction.url.absoluteString)" 58 | } 59 | return shareAction.url 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Common/FormInputText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormInputText.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/6/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FormInputText: View { 11 | var name: String 12 | var height: CGFloat = 100 13 | @Binding var text: String 14 | 15 | var body: some View { 16 | MultilineTextField(name, text: $text, height: height) 17 | .font(.system(size: 15)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Common/ImagePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePicker.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 12/25/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ImagePicker: UIViewControllerRepresentable { 11 | @Environment(\.presentationMode) var presentationMode 12 | var select: (UIImage) -> Void 13 | 14 | var allowsEditing: Bool = true 15 | 16 | class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { 17 | let parent: ImagePicker 18 | 19 | init(_ parent: ImagePicker) { 20 | self.parent = parent 21 | } 22 | 23 | func imagePickerController( 24 | _ picker: UIImagePickerController, 25 | didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any] 26 | ) { 27 | DispatchQueue.main.async { 28 | if self.parent.allowsEditing { 29 | if let uiImage = info[.editedImage] as? UIImage { 30 | self.parent.select(uiImage) 31 | } 32 | } else if let uiImage = info[.originalImage] as? UIImage { 33 | self.parent.select(uiImage) 34 | } 35 | hideKeyboard() 36 | self.parent.presentationMode.wrappedValue.dismiss() 37 | } 38 | } 39 | 40 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { 41 | hideKeyboard() 42 | parent.presentationMode.wrappedValue.dismiss() 43 | } 44 | } 45 | 46 | func makeCoordinator() -> Coordinator { 47 | Coordinator(self) 48 | } 49 | 50 | func makeUIViewController( 51 | context: UIViewControllerRepresentableContext 52 | ) -> UIImagePickerController { 53 | let picker = UIImagePickerController() 54 | picker.delegate = context.coordinator 55 | picker.allowsEditing = allowsEditing 56 | return picker 57 | } 58 | 59 | func updateUIViewController( 60 | _ uiViewController: UIImagePickerController, 61 | context: UIViewControllerRepresentableContext 62 | ) {} 63 | } 64 | 65 | struct ImagePicker_Previews: PreviewProvider { 66 | @State static var image: UIImage? 67 | 68 | static var previews: some View { 69 | ImagePicker(select: {_ in}) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Common/LargeButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LargeButton.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/3/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LargeButton: View { 11 | 12 | let content: Content 13 | let fontSize: CGFloat 14 | 15 | init(_ text: String, fontSize: CGFloat = 24) where Content == Text { 16 | self.content = Text(text) 17 | self.fontSize = fontSize 18 | } 19 | 20 | init(@ViewBuilder _ content: @escaping () -> Content, fontSize: CGFloat = 24) { 21 | self.content = content() 22 | self.fontSize = fontSize 23 | } 24 | 25 | var body: some View { 26 | content 27 | .foregroundColor(Color("foreground")) 28 | .font(.system(size: fontSize)) 29 | .frame(minWidth: 0, maxWidth: .infinity) 30 | .frame(height: 60) 31 | .background(RoundedRectangle(cornerRadius: 10) 32 | .stroke(Colors.linearGradient, style: StrokeStyle(lineWidth: 4)) 33 | .background(Color("background"))) 34 | .cornerRadius(10) 35 | .shadow(radius: 5) 36 | } 37 | } 38 | 39 | struct LargeButton_Previews: PreviewProvider { 40 | static var previews: some View { 41 | LargeButton("Click me") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Common/NavTitle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavTitle.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 1/2/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NavTitle: View { 11 | let title: String 12 | init(_ title: String) { 13 | self.title = title 14 | } 15 | 16 | var body: some View { 17 | Text(title) 18 | .fontWeight(.semibold) 19 | .font(.system(size: 18)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Common/RefreshableScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RefreshableScrollView.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 4/15/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RefreshableScrollView: View { 11 | var spacing: CGFloat? 12 | var content: () -> Content 13 | var onRefresh: OnRefresh 14 | var onLoadMore: OnLoadMore? 15 | 16 | init( 17 | spacing: CGFloat? = nil, 18 | @ViewBuilder content: @escaping () -> Content, 19 | onRefresh: @escaping OnRefresh, 20 | onLoadMore: OnLoadMore? = nil 21 | ) { 22 | self.spacing = spacing 23 | self.content = content 24 | self.onRefresh = onRefresh 25 | self.onLoadMore = onLoadMore 26 | } 27 | 28 | var body: some View { 29 | ScrollView(showsIndicators: false) { 30 | LazyVStack(spacing: spacing) { 31 | content() 32 | Color("background") 33 | .frame(height: UIScreen.main.bounds.height * 0.2) 34 | .onAppear { 35 | onLoadMore?() 36 | } 37 | Spacer() 38 | } 39 | } 40 | .refreshable { 41 | onRefresh({}) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Common/SearchBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBar.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 11/9/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SearchBar: View { 11 | @Binding var text: String 12 | var isActive: FocusState.Binding 13 | 14 | var placeholder: String = "Search" 15 | var disableAutocorrection: Bool = false 16 | var onCommit: () -> Void 17 | 18 | var body: some View { 19 | HStack { 20 | TextField(placeholder, text: $text, onCommit: onCommit) 21 | .focused(isActive) 22 | .disableAutocorrection(disableAutocorrection) 23 | .textContentType(.location) 24 | .padding(8) 25 | .padding(.horizontal, 25) 26 | .background(Color(.systemGray5)) 27 | .cornerRadius(10) 28 | .overlay( 29 | HStack { 30 | Image(systemName: "magnifyingglass") 31 | .foregroundColor(.gray) 32 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) 33 | .padding(.leading, 8) 34 | 35 | if isActive.wrappedValue { 36 | Button(action: { 37 | withAnimation { 38 | self.text = "" 39 | } 40 | }) { 41 | Image(systemName: "multiply.circle.fill") 42 | .foregroundColor(.gray) 43 | .padding(.trailing, 8) 44 | } 45 | } 46 | } 47 | ) 48 | 49 | if isActive.wrappedValue { 50 | Button(action: { 51 | withAnimation { 52 | DispatchQueue.main.async { 53 | self.isActive.wrappedValue = false 54 | self.text = "" 55 | } 56 | } 57 | }) { 58 | Text("Cancel") 59 | .foregroundColor(.blue) 60 | } 61 | .padding(.trailing, 10) 62 | .transition(.move(edge: .trailing)) 63 | } 64 | } 65 | .padding(.vertical, 6) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Common/TextAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextAlert.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/19/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TextAlert: UIViewControllerRepresentable { 11 | @State private var text = "" 12 | @Binding var isPresented: Bool 13 | var title: String 14 | var message: String 15 | var submitText = "Submit" 16 | var action: (String) -> Void 17 | 18 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIViewController { 19 | return UIViewController() 20 | } 21 | 22 | func updateUIViewController(_ viewController: UIViewController, context: UIViewControllerRepresentableContext) { 23 | guard context.coordinator.alert == nil else { return } 24 | if isPresented { 25 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 26 | context.coordinator.alert = alert 27 | alert.addTextField { textField in 28 | textField.placeholder = "Details" 29 | textField.autocapitalizationType = .sentences 30 | textField.delegate = context.coordinator 31 | } 32 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in }) 33 | alert.addAction(UIAlertAction(title: submitText, style: .default) { _ in 34 | action(text) 35 | text = "" 36 | }) 37 | DispatchQueue.main.async { 38 | viewController.present(alert, animated: true, completion: { 39 | self.isPresented = false 40 | context.coordinator.alert = nil 41 | }) 42 | } 43 | } 44 | } 45 | 46 | func makeCoordinator() -> TextAlert.Coordinator { 47 | Coordinator(self) 48 | } 49 | 50 | class Coordinator: NSObject, UITextFieldDelegate { 51 | var alert: UIAlertController? 52 | var parent: TextAlert 53 | init(_ parent: TextAlert) { 54 | self.parent = parent 55 | } 56 | 57 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 58 | if let text = textField.text as NSString? { 59 | self.parent.text = text.replacingCharacters(in: range, with: string) 60 | } else { 61 | self.parent.text = "" 62 | } 63 | return true 64 | } 65 | } 66 | } 67 | 68 | extension View { 69 | func textAlert( 70 | isPresented: Binding, 71 | title: String, 72 | message: String, 73 | submitText: String = "Submit", 74 | action: @escaping (String) -> Void 75 | ) -> some View { 76 | ZStack { 77 | TextAlert(isPresented: isPresented, title: title, message: message, submitText: submitText, action: action) 78 | self 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Common/Toast.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Toast.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 12/28/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum ToastType { 11 | case success, warning, error 12 | 13 | func color() -> Color { 14 | switch self { 15 | case .success: 16 | return Color(red: 0.15, green: 0.83, blue: 0.3) 17 | case .warning: 18 | return Color(red: 1, green: 0.7, blue: 0) 19 | case .error: 20 | return Color(red: 0.85, green: 0.2, blue: 0.15) 21 | } 22 | } 23 | } 24 | 25 | struct Toast: View { 26 | 27 | let text: String 28 | let type: ToastType 29 | 30 | var body: some View { 31 | Text(text) 32 | .font(.system(size: 14)) 33 | .foregroundColor(.white) 34 | .padding(15) 35 | .background(type.color()) 36 | .cornerRadius(15) 37 | .padding(.bottom, 40) 38 | .shadow(radius: 5) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Common/URLImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLImage.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 11/8/20. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | 11 | struct URLImage: View { 12 | var url: String? 13 | var loading: Image? 14 | var thumbnail: Bool = false 15 | 16 | var realUrl: URL? { 17 | if let url = url { 18 | return URL(string: url) 19 | } 20 | return nil 21 | } 22 | 23 | var maxDim: CGFloat { 24 | thumbnail ? 500 : 3000 25 | } 26 | 27 | var body: some View { 28 | WebImage( 29 | url: realUrl, 30 | context: [.imageThumbnailPixelSize: CGSize(width: maxDim, height: maxDim)] 31 | ) { image in 32 | image.resizable() 33 | } placeholder: { 34 | if let view = loading { 35 | AnyView(view.resizable()) 36 | } else { 37 | AnyView(Color("background").opacity(0.9)) 38 | } 39 | } 40 | .transition(.fade(duration: 0.1)) 41 | .scaledToFill() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/DeepLinking/DeepLinkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeepLinkManager.swift 3 | // Jimo 4 | // 5 | // Created by Xilin Liu on 4/20/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class DeepLinkManager: ObservableObject { 11 | @Published var presentableEntity: DeepLinkEntity? { 12 | didSet { 13 | if presentableEntity != oldValue && presentableEntity != nil { 14 | navigationState.push(.deepLink(entity: presentableEntity!)) 15 | } 16 | } 17 | } 18 | var navigationState = NavigationState() 19 | } 20 | 21 | /// What type of detail page we want to open based on the deeplink URL 22 | enum DeepLinkEntity: Identifiable, Hashable { 23 | case profile(String), post(PostId), loadedPost(Post) 24 | 25 | @ViewBuilder 26 | func view() -> some View { 27 | switch self { 28 | case .profile(let username): 29 | DeepLinkProfileLoadingScreen(username: username).id(username) 30 | case .post(let postId): 31 | DeepLinkViewPost(postId: postId) 32 | case .loadedPost(let post): 33 | ViewPost(initialPost: post) 34 | } 35 | } 36 | 37 | var id: String { 38 | switch self { 39 | case .profile(let username): 40 | return "profile-\(username)" 41 | case .post(let postId): 42 | return "postId-\(postId)" 43 | case .loadedPost(let post): 44 | return "post-\(post.postId)" 45 | } 46 | } 47 | } 48 | 49 | extension URL { 50 | private struct Constants { 51 | static let deeplinkHost = "go.jimoapp.com" 52 | 53 | static let profileDeeplinkPath = "/view-profile" 54 | static let profileQueryParam = "username" 55 | 56 | static let postDeeplinkPath = "/view-post" 57 | static let postQueryParam = "id" 58 | } 59 | 60 | /// checks whether URL is a deeplink, prefixed by our custom app scheme 61 | var isDeepLink: Bool { host == Constants.deeplinkHost } 62 | 63 | /// Decodes the entity type and entity Id from the deeplink 64 | /// e.g. `https://go.jimoapp.com/view-profile?username=` 65 | var entityType: DeepLinkEntity? { 66 | guard isDeepLink, 67 | let urlComponents = URLComponents(string: absoluteString) 68 | else { return .none } 69 | 70 | switch urlComponents.path { 71 | case Constants.profileDeeplinkPath: 72 | guard let username = urlComponents.queryItems?.first(where: { $0.name == Constants.profileQueryParam })?.value 73 | else { return .none } 74 | return .profile(username) 75 | case Constants.postDeeplinkPath: 76 | guard let postUuid = urlComponents.queryItems?.first(where: { $0.name == Constants.postQueryParam })?.value 77 | else { return .none } 78 | return .post(postUuid) 79 | default: return .none 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/DeepLinking/DeepLinkProfileLoadingScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeepLinkProfileLoadingScreen.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 7/26/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// This wrapper view loads the user, if we have not done so yet 11 | struct DeepLinkProfileLoadingScreen: View { 12 | @Environment(\.presentationMode) var presentationMode 13 | @EnvironmentObject var appState: AppState 14 | @EnvironmentObject var viewState: GlobalViewState 15 | @StateObject var viewModel = ViewModel() 16 | 17 | var username: String 18 | 19 | var body: some View { 20 | Group { 21 | if appState.currentUser.isAnonymous { 22 | VStack { 23 | Text("Account required") 24 | Button { 25 | viewState.showSignUpPage(.deepLinkProfile) 26 | } label: { 27 | Text("Sign up to view \(username)'s profile") 28 | .foregroundColor(.blue) 29 | } 30 | } 31 | } else if let user = viewModel.initialUser { 32 | ProfileScreen(initialUser: user) 33 | } else if viewModel.loadStatus == .notInitialized { 34 | ProgressView() 35 | .onAppear { 36 | viewModel.loadProfile(with: appState, viewState: viewState, username: username) 37 | } 38 | } else { 39 | ProgressView() 40 | .onAppear { 41 | presentationMode.wrappedValue.dismiss() 42 | } 43 | } 44 | } 45 | .background(Color("background")) 46 | .navigationBarTitleDisplayMode(.inline) 47 | .navigationBarColor(UIColor(Color("background"))) 48 | .navigationTitle(Text("Profile")) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/DeepLinking/DeepLinkViewPost.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeepLinkViewPost.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 4/26/22. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | struct DeepLinkViewPost: View { 12 | @Environment(\.presentationMode) var presentationMode 13 | @EnvironmentObject var appState: AppState 14 | @EnvironmentObject var viewState: GlobalViewState 15 | @StateObject var viewModel = ViewModel() 16 | 17 | let postId: PostId 18 | 19 | var body: some View { 20 | Group { 21 | if appState.currentUser.isAnonymous { 22 | VStack { 23 | Text("Account required") 24 | Button { 25 | viewState.showSignUpPage(.deepLinkPost) 26 | } label: { 27 | Text("Sign up to view this post") 28 | .foregroundColor(.blue) 29 | } 30 | } 31 | } else { 32 | switch viewModel.loadStatus { 33 | case .loading: 34 | ProgressView() 35 | .onAppear { 36 | viewModel.load(postId, appState: appState, viewState: viewState) 37 | } 38 | case .failed: 39 | ProgressView() 40 | .onAppear { 41 | presentationMode.wrappedValue.dismiss() 42 | } 43 | case .success(let post): 44 | ViewPost(initialPost: post) 45 | } 46 | } 47 | } 48 | .background(Color("background")) 49 | .navigationBarTitleDisplayMode(.inline) 50 | .navigationBarColor(UIColor(Color("background"))) 51 | .navigationTitle(Text("View Post")) 52 | } 53 | } 54 | 55 | extension DeepLinkViewPost { 56 | class ViewModel: ObservableObject { 57 | @Published var loadStatus: Status = .loading 58 | 59 | var cancelBag: Set = .init() 60 | 61 | func load(_ postId: PostId, appState: AppState, viewState: GlobalViewState) { 62 | self.loadStatus = .loading 63 | appState.getPost(postId) 64 | .sink { [weak self] completion in 65 | if case let .failure(error) = completion { 66 | print("Error when loading post", error) 67 | viewState.setError("Post not found") 68 | self?.loadStatus = .failed 69 | } 70 | } receiveValue: { [weak self] post in 71 | self?.loadStatus = .success(post) 72 | }.store(in: &cancelBag) 73 | } 74 | 75 | enum Status: Equatable { 76 | case loading, success(Post), failed 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Discover/DiscoverViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiscoverViewModel.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/8/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import CoreLocation 11 | 12 | class DiscoverViewModel: ObservableObject { 13 | let locationManager = CLLocationManager() 14 | let nc = NotificationCenter.default 15 | 16 | @Published var posts: [Post] = [] 17 | @Published var initialized = false 18 | 19 | private var loadFeedCancellable: Cancellable? 20 | 21 | var maybeLocation: Location? { 22 | if let location = locationManager.location { 23 | return Location(coord: location.coordinate) 24 | } else { 25 | return nil 26 | } 27 | } 28 | 29 | init() { 30 | nc.addObserver(self, selector: #selector(postLiked), name: PostPublisher.postLiked, object: nil) 31 | nc.addObserver(self, selector: #selector(placeSaved), name: PlacePublisher.placeSaved, object: nil) 32 | nc.addObserver(self, selector: #selector(postUpdated), name: PostPublisher.postUpdated, object: nil) 33 | nc.addObserver(self, selector: #selector(postDeleted), name: PostPublisher.postDeleted, object: nil) 34 | } 35 | 36 | @objc private func placeSaved(notification: Notification) { 37 | let payload = notification.object as! PlaceSavePayload 38 | let postIndex = posts.indices.first(where: { posts[$0].place.placeId == payload.placeId }) 39 | if let i = postIndex { 40 | posts[i].saved = payload.save != nil 41 | } 42 | } 43 | 44 | @objc private func postLiked(notification: Notification) { 45 | let like = notification.object as! PostLikePayload 46 | let postIndex = posts.indices.first(where: { posts[$0].postId == like.postId }) 47 | if let i = postIndex { 48 | posts[i].likeCount = like.likeCount 49 | posts[i].liked = like.liked 50 | } 51 | } 52 | 53 | @objc private func postUpdated(notification: Notification) { 54 | let post = notification.object as! Post 55 | if let i = posts.indices.first(where: { posts[$0].postId == post.postId }) { 56 | posts[i] = post 57 | } 58 | } 59 | 60 | @objc private func postDeleted(notification: Notification) { 61 | let postId = notification.object as! PostId 62 | posts.removeAll(where: { $0.postId == postId }) 63 | } 64 | 65 | func loadDiscoverPage(appState: AppState, onFinish: OnFinish? = nil) { 66 | loadFeedCancellable = appState.discoverFeedV2(location: maybeLocation) 67 | .sink(receiveCompletion: { [weak self] completion in 68 | self?.initialized = true 69 | onFinish?() 70 | if case let .failure(error) = completion { 71 | print("Error when loading posts", error) 72 | } 73 | }, receiveValue: { [weak self] feed in 74 | self?.posts = feed.posts 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Discover/SearchViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewModel.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/8/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | class SearchViewModel: ObservableObject { 12 | @Published var query: String = "" 13 | @Published var userResults: [PublicUser] = [] 14 | 15 | private var queryCancellable: Cancellable? 16 | private var userSearchCancellable: Cancellable? 17 | 18 | func listen(appState: AppState) { 19 | userSearchCancellable = $query 20 | .debounce(for: 0.5, scheduler: DispatchQueue.main) 21 | .sink { [weak self] query in 22 | self?.search(appState: appState, query: query) 23 | } 24 | } 25 | 26 | private func search(appState: AppState, query: String) { 27 | queryCancellable = appState.searchUsers(query: query) 28 | .catch { error -> AnyPublisher<[PublicUser], Never> in 29 | print("Error when searching", error) 30 | return Empty().eraseToAnyPublisher() 31 | } 32 | .sink { [weak self] results in 33 | self?.userResults = results 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Feedback/Checkbox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Checkbox.swift 3 | // Jimo 4 | // 5 | // Created by Jeff Rohlman on 2/28/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Checkbox: View { 11 | let label: String 12 | let boxSize: CGFloat 13 | @Binding var selected: Bool 14 | 15 | var body: some View { 16 | Button(action: { 17 | selected.toggle() 18 | }) { 19 | HStack(alignment: .center, spacing: 10) { 20 | Image(systemName: self.selected ? "checkmark.square" : "square") 21 | .resizable() 22 | .aspectRatio(contentMode: .fit) 23 | .frame(width: boxSize, height: boxSize) 24 | .foregroundColor(Color("foreground")) 25 | Text(label) 26 | .font(.system(size: 12)) 27 | .multilineTextAlignment(.leading) 28 | Spacer() 29 | } 30 | } 31 | .foregroundColor(Color("foreground")) 32 | .padding(.horizontal, 15) 33 | .padding(.vertical, 10) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Feedback/RoundedButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoundedButton.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 11/7/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RoundedButton: View { 11 | let text: Text 12 | let action: () -> Void 13 | let backgroundColor: Color 14 | 15 | var body: some View { 16 | Button(action: action) { 17 | text 18 | .foregroundColor(.black) 19 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) 20 | .background( 21 | RoundedRectangle(cornerRadius: 100).fill(backgroundColor) 22 | .shadow(radius: 3)) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Feeds/DeepLinkableFeedTab.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeepLinkableFeedTab.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/13/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DeepLinkableFeedTab: View { 11 | @EnvironmentObject var deepLinkManager: DeepLinkManager 12 | @EnvironmentObject var appState: AppState 13 | @EnvironmentObject var globalViewState: GlobalViewState 14 | @ObservedObject var navigationState: NavigationState 15 | 16 | @ObservedObject var notificationsModel: NotificationBadgeModel 17 | @StateObject private var notificationFeedVM = NotificationFeedViewModel() 18 | 19 | var onCreatePostTap: () -> Void 20 | 21 | var showBadge: Bool { 22 | notificationsModel.unreadNotifications > 0 || notificationFeedVM.shouldRequestNotificationPermissions 23 | } 24 | 25 | var body: some View { 26 | Navigator(state: navigationState) { 27 | FeedTabBody( 28 | notificationBellBadgePresent: showBadge, 29 | onCreatePostTap: { globalViewState.createPostPresented = true } 30 | ) 31 | } 32 | .onChange(of: deepLinkManager.navigationState.path) { path in 33 | print("navigation path updated to \(path)") 34 | } 35 | .environmentObject(appState) 36 | .environmentObject(globalViewState) 37 | .environmentObject(notificationFeedVM) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Feeds/FeedItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedItem.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 4/4/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FeedItem: View { 11 | @EnvironmentObject var appState: AppState 12 | @EnvironmentObject var globalViewState: GlobalViewState 13 | 14 | @StateObject var postVM = PostVM() 15 | 16 | var post: Post 17 | var navigate: (NavDestination) -> Void 18 | var showShareSheet: () -> Void 19 | 20 | var body: some View { 21 | VStack { 22 | PostHeader( 23 | postVM: postVM, 24 | post: post, 25 | navigate: { self.navigate(.profile(user: $0)) }, 26 | showShareSheet: showShareSheet 27 | ) 28 | 29 | PostImage(post: post) 30 | .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width) 31 | .contentShape(Rectangle()) 32 | .clipped() 33 | .onTapGesture { 34 | self.navigate(.post(post: post)) 35 | } 36 | 37 | VStack(spacing: 5) { 38 | PostPlaceName(post: post).onTapGesture { 39 | Analytics.track(.postPlaceNameTap) 40 | self.navigate(.liteMapView(post: post)) 41 | } 42 | PostCaption(post: post) 43 | .lineLimit(3) 44 | .onTapGesture { 45 | self.navigate(.post(post: post)) 46 | } 47 | } 48 | PostFooter( 49 | viewModel: postVM, 50 | post: post, 51 | onCommentTap: { self.navigate(.post(post: post)) } 52 | ) 53 | 54 | Rectangle() 55 | .frame(maxWidth: .infinity) 56 | .frame(height: 8) 57 | .foregroundColor(Color("foreground").opacity(0.1)) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/FirstOpenPopup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirstOpenPopup.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FirstOpenPopup: View { 11 | @Binding var isPresented: Bool 12 | let goToProfile: () -> Void 13 | 14 | var body: some View { 15 | VStack(spacing: 12) { 16 | Text("Welcome to Jimo!") 17 | .foregroundColor(Color("foreground")) 18 | .font(.system(size: 20)) 19 | 20 | welcomeText 21 | .font(.system(size: 12)) 22 | .foregroundColor(Color("foreground")) 23 | .opacity(0.6) 24 | .multilineTextAlignment(.center) 25 | .padding(.bottom, 12) 26 | 27 | buttons 28 | } 29 | .padding(EdgeInsets(top: 37, leading: 24, bottom: 40, trailing: 24)) 30 | .background(Color("background").cornerRadius(10)) 31 | .padding(.horizontal, 16) 32 | } 33 | 34 | var welcomeText: Text { 35 | Text("We're so glad you're here. " + 36 | "Track your favorite places, save ones you want to visit, " + 37 | "and discover new spots through other people's recommendations." + 38 | "" 39 | ) 40 | } 41 | 42 | @ViewBuilder 43 | var buttons: some View { 44 | Button { 45 | isPresented = false 46 | } label: { 47 | Text("Start exploring").bold() 48 | .frame(maxWidth: .infinity) 49 | .frame(height: 50) 50 | .foregroundColor(.white) 51 | .background(Color("lodging")) 52 | .cornerRadius(10) 53 | } 54 | 55 | Button { 56 | DispatchQueue.main.async { 57 | isPresented = false 58 | goToProfile() 59 | } 60 | } label: { 61 | Text("Go to my profile").bold() 62 | .frame(maxWidth: .infinity) 63 | .frame(height: 50) 64 | .foregroundColor(Color("lodging")) 65 | .overlay( 66 | RoundedRectangle(cornerRadius: 10).stroke(Color("lodging"), lineWidth: 2) 67 | ) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/GuestAccount/MaybeGuestPostPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaybeGuestPostPage.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/1/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MaybeGuestPostPage: View { 11 | @EnvironmentObject var appState: AppState 12 | @EnvironmentObject var viewState: GlobalViewState 13 | @EnvironmentObject var navigationState: NavigationState 14 | 15 | var post: Post 16 | var showSignUpAlert: (SignUpTapSource) -> Void 17 | 18 | var body: some View { 19 | if appState.currentUser.isAnonymous { 20 | PostPage(post: post) 21 | .disabled(true) 22 | .onTapGesture { 23 | showSignUpAlert(.placeDetailsViewPost) 24 | } 25 | } else { 26 | PostPage(post: post) 27 | .onTapGesture { 28 | navigationState.push(.post(post: post, showSaveButton: false)) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/GuestAccount/PostPagePlaceholder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostPagePlaceholder.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/1/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PostPagePlaceholder: View { 11 | var body: some View { 12 | HStack(alignment: .top) { 13 | Rectangle() 14 | .foregroundColor(.gray) 15 | .frame(width: 120, height: 120) 16 | .cornerRadius(2) 17 | 18 | VStack(alignment: .leading, spacing: 5) { 19 | Text("Place name") 20 | .font(.caption) 21 | .fontWeight(.black) 22 | .lineLimit(1) 23 | 24 | Group { 25 | Text("username") 26 | .font(.caption) 27 | .fontWeight(.bold) 28 | + 29 | Text(String(repeating: "This could be you heree ", count: 4)) 30 | .font(.caption) 31 | } 32 | 33 | Spacer() 34 | 35 | HStack(spacing: 5) { 36 | Image(systemName: "heart") 37 | .font(.system(size: 15)) 38 | Text("100").font(.caption) 39 | 40 | Spacer().frame(width: 2) 41 | 42 | Image(systemName: "bubble.right") 43 | .font(.system(size: 15)) 44 | .offset(y: 1.5) 45 | Text("50").font(.caption) 46 | 47 | Spacer() 48 | } 49 | .foregroundColor(Color("foreground")) 50 | } 51 | .frame(height: 120) 52 | Spacer() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/GuestAccount/SignUpTapSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpTapSource.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/1/23. 6 | // 7 | 8 | enum SignUpTapSource: String { 9 | case none, 10 | feed, 11 | searchUsers, 12 | profile, 13 | filterSaves, 14 | filterMe, 15 | filterFriends, 16 | filterCommunity, 17 | placeDetailsNudge, 18 | placeDetailsPost, 19 | placeDetailsSave, 20 | placeDetailsViewPost, 21 | placeDetailsCommunityNudge, 22 | createPost, 23 | customUserFilter, 24 | deepLinkProfile, 25 | deepLinkPost 26 | 27 | // Only set if the source displays a sign up nudge alert (feed and profile go directly to sign up page) 28 | var signUpNudgeText: String? { 29 | switch self { 30 | case .none: return nil 31 | case .feed: return nil 32 | case .searchUsers: return "Sign up to search and add friends." 33 | case .profile: return nil 34 | case .filterSaves: return "Sign up to start saving places." 35 | case .filterMe: return "Sign up to start posting places." 36 | case .filterFriends: return "Sign up to follow and invite friends." 37 | case .filterCommunity: return "Sign up to view the community map." 38 | case .placeDetailsNudge: return nil 39 | case .placeDetailsPost: return "Sign up to start posting places." 40 | case .placeDetailsSave: return "Sign up to start saving places." 41 | case .placeDetailsViewPost: return "Sign up to interact with posts." 42 | case .placeDetailsCommunityNudge: return nil 43 | case .createPost: return "Sign up to start posting places." 44 | case .customUserFilter: return nil 45 | case .deepLinkPost: return nil 46 | case .deepLinkProfile: return nil 47 | } 48 | } 49 | 50 | var analyticsSourceParameter: String { 51 | self.rawValue 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Map/BottomSheet/CategoryFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoryFilter.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 1/7/22. 6 | // 7 | 8 | import SwiftUI 9 | import BottomSheet 10 | 11 | private struct CategoryView: View { 12 | @Binding var selected: Set 13 | 14 | var category: Category 15 | 16 | var key: String { 17 | category.key 18 | } 19 | 20 | var isSelected: Bool { 21 | selected.contains(category) 22 | } 23 | 24 | var allSelected: Bool { 25 | selected.count == Categories.categories.count 26 | } 27 | 28 | var onlySelected: Bool { 29 | isSelected && selected.count == 1 30 | } 31 | 32 | var body: some View { 33 | HStack { 34 | Image(key) 35 | .resizable() 36 | .scaledToFit() 37 | .frame(maxWidth: 35, maxHeight: 35) 38 | 39 | Spacer() 40 | 41 | Text(category.name) 42 | .font(.system(size: 15)) 43 | .foregroundColor(.black) 44 | } 45 | .padding(.horizontal, 10) 46 | .padding(.vertical, 7.5) 47 | .background(isSelected ? Color(key) : Color("unselected")) 48 | .cornerRadius(2) 49 | .shadow(radius: isSelected ? 5 : 0) 50 | .frame(height: 50) 51 | .onTapGesture { 52 | if allSelected { 53 | self.selected = [category] 54 | } else if onlySelected { 55 | self.selected = Set(Categories.categories) 56 | } else if isSelected { 57 | self.selected.remove(category) 58 | } else { 59 | self.selected.insert(category) 60 | } 61 | } 62 | } 63 | } 64 | 65 | struct CategoryFilter: View { 66 | @Binding var selected: Set 67 | 68 | var body: some View { 69 | VStack { 70 | HStack { 71 | Text("Filter pins by category") 72 | .font(.system(size: 15)) 73 | .bold() 74 | Spacer() 75 | } 76 | 77 | VStack { 78 | HStack { 79 | CategoryView(selected: $selected, category: Categories.categories[0]) 80 | CategoryView(selected: $selected, category: Categories.categories[1]) 81 | } 82 | 83 | HStack { 84 | CategoryView(selected: $selected, category: Categories.categories[2]) 85 | CategoryView(selected: $selected, category: Categories.categories[3]) 86 | } 87 | 88 | HStack { 89 | CategoryView(selected: $selected, category: Categories.categories[4]) 90 | CategoryView(selected: $selected, category: Categories.categories[5]) 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Map/BottomSheet/MapBottomSheetHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapBottomSheetHeader.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/22/22. 6 | // 7 | 8 | import SwiftUI 9 | import BottomSheet 10 | 11 | struct MapBottomSheetHeader: View { 12 | @ObservedObject var locationSearch: LocationSearch 13 | var searchFieldActive: FocusState.Binding 14 | 15 | var body: some View { 16 | MapSearchField(text: $locationSearch.searchQuery, isActive: searchFieldActive, placeholder: "Search a place", onCommit: { 17 | locationSearch.search() 18 | }) 19 | .ignoresSafeArea(.keyboard, edges: .all) 20 | .padding(.horizontal, 10) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Map/BottomSheet/MapSearchField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapSearchField.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 1/18/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MapSearchField: View { 11 | @Binding var text: String 12 | var isActive: FocusState.Binding 13 | 14 | var placeholder: String = "Search" 15 | 16 | var onCommit: () -> Void 17 | 18 | var body: some View { 19 | HStack { 20 | HStack(spacing: 5) { 21 | Image(systemName: "magnifyingglass") 22 | .foregroundColor(.gray) 23 | 24 | TextField(placeholder, text: $text, onCommit: onCommit) 25 | .textContentType(.location) 26 | .focused(isActive) 27 | .submitLabel(.search) 28 | .frame(maxWidth: .infinity) 29 | 30 | if isActive.wrappedValue { 31 | Button(action: { 32 | withAnimation { 33 | self.text = "" 34 | } 35 | }) { 36 | Image(systemName: "multiply.circle.fill") 37 | .foregroundColor(.gray) 38 | } 39 | } 40 | } 41 | .padding(8) 42 | .background(Color("foreground").opacity(0.1)) 43 | .cornerRadius(10) 44 | 45 | if isActive.wrappedValue { 46 | Button(action: { 47 | withAnimation { 48 | self.isActive.wrappedValue = false 49 | self.text = "" 50 | } 51 | hideKeyboard() 52 | }) { 53 | Text("Cancel") 54 | .foregroundColor(.blue) 55 | } 56 | } 57 | } 58 | .padding(.vertical, 6) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Map/Components/CircularCheckbox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircularCheckbox.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 1/31/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CircularCheckbox: View { 11 | var selected: Bool 12 | 13 | var imageName: String { 14 | selected ? "checkmark.circle.fill" : "circle" 15 | } 16 | 17 | var body: some View { 18 | Image(systemName: imageName) 19 | .font(.system(size: 30, weight: .light)) 20 | .foregroundColor(.blue) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Map/Components/GlobalViewFilterButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalViewFilterButton.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 1/31/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GlobalViewFilterButton: View { 11 | var body: some View { 12 | ZStack { 13 | Image("logo") 14 | .resizable() 15 | .scaledToFit() 16 | .frame(maxWidth: .infinity, maxHeight: .infinity) 17 | .padding(5) 18 | Circle() 19 | .stroke(Colors.angularGradient, style: StrokeStyle(lineWidth: 2.5)) 20 | .frame(maxWidth: .infinity, maxHeight: .infinity) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Map/Components/SignUpAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpAlert.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/1/23. 6 | // 7 | 8 | struct SignUpAlert: Equatable { 9 | var isPresented: Bool 10 | var source: SignUpTapSource 11 | } 12 | // 13 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Map/Core/BlurBackground.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlurBackground.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/11/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BlurBackground: UIViewRepresentable { 11 | var effect: UIVisualEffect 12 | 13 | func makeUIView(context: Context) -> UIVisualEffectView { 14 | let effectView = UIVisualEffectView(effect: self.effect) 15 | return effectView 16 | } 17 | 18 | func updateUIView(_ uiView: UIVisualEffectView, context: Context) { 19 | uiView.effect = self.effect 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Map/Core/CurrentLocationButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CurrentLocationButton.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/22/22. 6 | // 7 | 8 | import SwiftUI 9 | import MapKit 10 | 11 | struct CurrentLocationButton: View { 12 | var setRegion: (MKCoordinateRegion) -> Void 13 | 14 | @State private var shouldRequestLocation = false 15 | 16 | var body: some View { 17 | Button(action: { 18 | UIImpactFeedbackGenerator(style: .medium).impactOccurred() 19 | withAnimation { 20 | if let location = PermissionManager.shared.getLocation() { 21 | setRegion(MKCoordinateRegion( 22 | center: location.coordinate, 23 | span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02) 24 | )) 25 | } else { 26 | shouldRequestLocation.toggle() 27 | print("Could not get location") 28 | } 29 | } 30 | }) { 31 | ZStack { 32 | Image(systemName: "location.fill") 33 | .foregroundColor(.blue) 34 | .font(.system(size: 20)) 35 | .frame(width: 50, height: 50) 36 | .background(Color("background")) 37 | .cornerRadius(25) 38 | .contentShape(Circle()) 39 | } 40 | } 41 | .buttonStyle(PlainButtonStyle()) 42 | .shadow(radius: 3) 43 | .fullScreenCover(isPresented: $shouldRequestLocation) { 44 | RequestLocation(onCompleteRequest: { self.shouldRequestLocation = false }) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Map/Core/LiteMapView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LiteMapView.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/25/22. 6 | // 7 | 8 | import SwiftUI 9 | import MapKit 10 | 11 | struct LiteMapView: View { 12 | @EnvironmentObject var appState: AppState 13 | @EnvironmentObject var viewState: GlobalViewState 14 | 15 | @StateObject var mapViewModel = MapViewModel() 16 | @StateObject var placeViewModel = PlaceDetailsViewModel() 17 | @StateObject var locationSearch = LocationSearch() 18 | @StateObject var sheetViewModel = SheetPositionViewModel() 19 | 20 | var post: Post 21 | 22 | var pin: MKJimoPinAnnotation { 23 | MKJimoPinAnnotation(from: MapPin( 24 | placeId: post.place.id, 25 | location: post.place.location, 26 | icon: MapPinIcon(category: post.category, iconUrl: post.user.profilePictureUrl, numPosts: 1) 27 | )) 28 | } 29 | 30 | var body: some View { 31 | BaseMapViewV2( 32 | placeViewModel: placeViewModel, 33 | mapViewModel: mapViewModel, 34 | locationSearch: locationSearch, 35 | sheetViewModel: sheetViewModel 36 | ) 37 | .onAppear { 38 | initializeMapViewModel() 39 | } 40 | } 41 | 42 | @MainActor func initializeMapViewModel() { 43 | mapViewModel.pins = [pin] 44 | mapViewModel.selectPin( 45 | placeViewModel: placeViewModel, 46 | appState: appState, 47 | viewState: viewState, 48 | pin: pin 49 | ) 50 | sheetViewModel.showBusinessSheet() 51 | mapViewModel.listenToRegionChanges(appState: appState, viewState: viewState) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Map/Core/MapTab.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapTab.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/18/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MapTab: View { 11 | @StateObject private var navigationState = NavigationState() 12 | 13 | var body: some View { 14 | Navigator(state: navigationState) { 15 | MapViewV2() 16 | .navigationBarHidden(true) 17 | .trackScreen(.mapTab) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Map/Core/RegionWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegionWrapper.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/22/22. 6 | // 7 | 8 | import SwiftUI 9 | import MapKit 10 | 11 | /// When using a regular @Binding panning the map is super laggy with a lot of pins 12 | /// (think the view gets rebuilt as the region value changes, so the annotations keep 13 | /// getting added, not sure). This class creates a region binding but allows us to change the value without 14 | /// triggering the binding. 15 | class RegionWrapper: ObservableObject { 16 | @Published var trigger = false 17 | var _region: MKCoordinateRegion = defaultRegion 18 | 19 | var region: Binding { 20 | Binding( 21 | get: { self._region }, 22 | set: { self._region = $0 } 23 | ) 24 | } 25 | 26 | func setRegion(_ region: MKCoordinateRegion) { 27 | self._region = region 28 | DispatchQueue.main.async { 29 | self.trigger.toggle() 30 | } 31 | } 32 | } 33 | 34 | private var defaultRegion = MKCoordinateRegion( 35 | center: CLLocationCoordinate2D(latitude: 37, longitude: -96), 36 | span: MKCoordinateSpan(latitudeDelta: 85, longitudeDelta: 61)) 37 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Map/Core/UIKitMap/MKJimoPinAnnotation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MKJimoPinAnnotation.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 7/9/22. 6 | // 7 | 8 | import UIKit 9 | import MapKit 10 | 11 | /** 12 | Utility class to make it easier for the MapPin type to interact with MapKit primitives. 13 | */ 14 | class MKJimoPinAnnotation: NSObject, MKAnnotation, Identifiable { 15 | 16 | var id: String { 17 | "\(placeId ?? ""):\(category ?? ""):\(imageUrl ?? ""):\(numPosts)" 18 | } 19 | 20 | // This property must be key-value observable, which the `@objc dynamic` attributes provide. 21 | @objc dynamic var coordinate: CLLocationCoordinate2D 22 | 23 | var placeId: PlaceId? 24 | 25 | var category: String? 26 | 27 | var imageUrl: String? 28 | 29 | var numPosts: Int = 0 30 | 31 | init(coordinate: CLLocationCoordinate2D) { 32 | self.coordinate = coordinate 33 | super.init() 34 | } 35 | 36 | init(from pin: MapPin) { 37 | self.coordinate = pin.location.coordinate() 38 | self.placeId = pin.placeId 39 | self.category = pin.icon.category 40 | self.imageUrl = pin.icon.iconUrl 41 | self.numPosts = pin.icon.numPosts 42 | super.init() 43 | } 44 | 45 | override func isEqual(_ object: Any?) -> Bool { 46 | if let annotation = object as? MKJimoPinAnnotation { 47 | return id == annotation.id 48 | } 49 | return false 50 | } 51 | 52 | public static func ==(lhs: MKJimoPinAnnotation, rhs: MKJimoPinAnnotation) -> Bool { 53 | return lhs.id == rhs.id 54 | } 55 | 56 | override var hash: Int { 57 | return id.hashValue 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Map/MapType+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapType+image.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 1/31/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension MapType { 11 | var buttonName: String { 12 | switch self { 13 | case .saved: return "Saved" 14 | case .me: return "My Posts" 15 | case .following: return "Following" 16 | case .community: return "Everyone" 17 | case .custom: return "Custom" 18 | } 19 | } 20 | 21 | var systemImage: String? { 22 | switch self { 23 | case .following: return "person.2.circle.fill" 24 | case .saved: return "bookmark.circle.fill" 25 | case .custom: return "ellipsis.circle.fill" 26 | default: return nil 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Notifications/NotificationFeedViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationFeedViewModel.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 7/26/22. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | class NotificationFeedViewModel: ObservableObject { 12 | @Published var feedItems: [NotificationItem] = [] 13 | @Published var loading = false 14 | @Published var shouldRequestNotificationPermissions = false 15 | 16 | private var cancellable: Cancellable? 17 | private var cursor: String? 18 | 19 | init() { 20 | PermissionManager.shared.getNotificationAuthStatus { status in 21 | DispatchQueue.main.async { 22 | self.shouldRequestNotificationPermissions = status != .authorized 23 | } 24 | } 25 | } 26 | 27 | func onAppear(appState: AppState, viewState: GlobalViewState) { 28 | self.refreshFeed(appState: appState, viewState: viewState) 29 | } 30 | 31 | func refreshFeed(appState: AppState, viewState: GlobalViewState, onFinish: OnFinish? = nil) { 32 | print("refreshing feed") 33 | cursor = nil 34 | loading = true 35 | cancellable = appState.getNotificationsFeed(token: nil) 36 | .sink(receiveCompletion: { [weak self] completion in 37 | self?.loading = false 38 | onFinish?() 39 | if case let .failure(error) = completion { 40 | print("Error while load notification feed.", error) 41 | viewState.setError("Could not load activity feed.") 42 | } 43 | }, receiveValue: { [weak self] response in 44 | self?.feedItems = response.notifications.filter { item in item.type != .unknown } 45 | self?.cursor = response.cursor 46 | }) 47 | } 48 | 49 | func loadMoreNotifications(appState: AppState, viewState: GlobalViewState) { 50 | guard cursor != nil else { 51 | return 52 | } 53 | guard !loading else { 54 | return 55 | } 56 | loading = true 57 | print("Loading more notifications") 58 | cancellable = appState.getNotificationsFeed(token: cursor) 59 | .sink(receiveCompletion: { [weak self] completion in 60 | self?.loading = false 61 | if case let .failure(error) = completion { 62 | print("Error while load more notifications.", error) 63 | viewState.setError("Could not load more items.") 64 | } 65 | }, receiveValue: { [weak self] response in 66 | self?.feedItems.append(contentsOf: response.notifications.filter { item in item.type != .unknown }) 67 | self?.cursor = response.cursor 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Onboarding/Components/PhoneNumberTextFieldView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhoneNumberTextFieldView.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/2/21. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | import PhoneNumberKit 11 | 12 | enum JimoPhoneNumberInput: Equatable { 13 | case number(PhoneNumber) 14 | case secretMenu 15 | } 16 | 17 | struct PhoneNumberTextFieldView: UIViewRepresentable { 18 | @Binding var phoneNumber: JimoPhoneNumberInput? 19 | private let textField = PhoneNumberTextField() 20 | 21 | func makeUIView(context: Context) -> PhoneNumberTextField { 22 | textField.placeholder = "(845) 462-5555" 23 | textField.withDefaultPickerUI = true 24 | textField.withFlag = true 25 | textField.withPrefix = true 26 | textField.textContentType = .telephoneNumber 27 | textField.addTarget(context.coordinator, action: #selector(Coordinator.onTextUpdate), for: .editingChanged) 28 | return textField 29 | } 30 | 31 | func updateUIView(_ view: PhoneNumberTextField, context: Context) { 32 | } 33 | 34 | func makeCoordinator() -> Coordinator { 35 | Coordinator(self) 36 | } 37 | 38 | class Coordinator: NSObject, UITextFieldDelegate { 39 | var parent: PhoneNumberTextFieldView 40 | 41 | init(_ parent: PhoneNumberTextFieldView) { 42 | self.parent = parent 43 | } 44 | 45 | @objc func onTextUpdate(textField: UITextField) { 46 | if let phoneNumber = parent.textField.phoneNumber { 47 | self.parent.phoneNumber = .number(phoneNumber) 48 | } else if textField.text == "546-6" { 49 | /// Hack to allow logging in with emails 50 | self.parent.phoneNumber = .secretMenu 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Onboarding/Components/SuggestedUserView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SuggestedUserView.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/14/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SuggestedUserView: View { 11 | @ObservedObject var userStore: T 12 | let user: PublicUser 13 | 14 | var profilePicture: URLImage { 15 | return URLImage(url: user.profilePictureUrl, 16 | loading: Image(systemName: "person.crop.circle").resizable()) 17 | } 18 | 19 | var body: some View { 20 | VStack { 21 | ZStack(alignment: .topTrailing) { 22 | profilePicture 23 | .foregroundColor(.gray) 24 | .background(Color.white) 25 | .scaledToFill() 26 | .frame(width: 80, height: 80) 27 | .cornerRadius(40) 28 | 29 | if userStore.selectedUsernames.contains(user.username) { 30 | Image("selectedContact") 31 | .resizable() 32 | .frame(width: 26, height: 26) 33 | .shadow(radius: 5) 34 | } 35 | } 36 | 37 | Text(user.firstName + " " + user.lastName) 38 | .font(.system(size: 12)) 39 | } 40 | .onTapGesture { 41 | userStore.toggleSelected(for: user.username) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Onboarding/Components/UserList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserList.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/14/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UserList: View { 11 | @EnvironmentObject var appState: AppState 12 | @ObservedObject var userStore: T 13 | 14 | var users: [PublicUser] { 15 | userStore.allUsers 16 | } 17 | 18 | private let columns: [GridItem] = [ 19 | GridItem(.flexible(minimum: 50), spacing: 10), 20 | GridItem(.flexible(minimum: 50), spacing: 10), 21 | GridItem(.flexible(minimum: 50), spacing: 10) 22 | ] 23 | 24 | var body: some View { 25 | VStack { 26 | ScrollView { 27 | LazyVGrid(columns: columns) { 28 | ForEach(users) { (user: PublicUser) in 29 | SuggestedUserView(userStore: userStore, user: user) 30 | } 31 | } 32 | .padding(.bottom, 50) 33 | } 34 | 35 | VStack { 36 | Button(action: { 37 | userStore.follow(appState: appState) 38 | }) { 39 | if userStore.followingLoading { 40 | LargeButton { 41 | ProgressView() 42 | } 43 | } else { 44 | LargeButton("Follow") 45 | } 46 | } 47 | .disabled(userStore.followingLoading) 48 | .padding(.horizontal, 40) 49 | .padding(.bottom, 5) 50 | 51 | Text("Clear selection") 52 | .font(.system(size: 16)) 53 | .foregroundColor(.gray) 54 | .onTapGesture { 55 | userStore.clearAll() 56 | } 57 | 58 | Text("Select all") 59 | .font(.system(size: 16)) 60 | .foregroundColor(.gray) 61 | .padding(.top, 2) 62 | .onTapGesture { 63 | userStore.selectAll() 64 | } 65 | } 66 | .padding(.top, 30) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Onboarding/FollowFeatured.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowFeatured.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/14/21. 6 | // 7 | 8 | import SwiftUI 9 | import PopupView 10 | 11 | struct FollowFeatured: View { 12 | @EnvironmentObject var appState: AppState 13 | 14 | @ObservedObject var onboardingModel: OnboardingModel 15 | @StateObject private var featuredUserStore = FeaturedUserStore() 16 | 17 | @ViewBuilder var viewBody: some View { 18 | VStack { 19 | HStack { 20 | Spacer() 21 | 22 | Text("Skip") 23 | .foregroundColor(.gray) 24 | .onTapGesture { 25 | onboardingModel.step() 26 | } 27 | } 28 | .padding(.vertical, 10) 29 | .padding(.horizontal, 30) 30 | 31 | Text("Featured Jimo Users") 32 | .font(.system(size: 24)) 33 | 34 | Spacer() 35 | 36 | if featuredUserStore.loadingSuggestedUsers { 37 | ProgressView() 38 | } else if featuredUserStore.loadingSuggestedUsersError != nil { 39 | VStack { 40 | Button(action: { 41 | featuredUserStore.getExistingUsers(appState: appState) 42 | }) { 43 | Text("Failed to load featured users, tap to try again.") 44 | .multilineTextAlignment(.center) 45 | } 46 | } 47 | .padding(.horizontal, 40) 48 | } else { 49 | UserList(userStore: featuredUserStore) 50 | } 51 | 52 | Spacer() 53 | } 54 | .padding(.bottom, 100) 55 | .onAppear { 56 | featuredUserStore.getExistingUsers(appState: appState) 57 | } 58 | .background(Color("background").edgesIgnoringSafeArea(.all)) 59 | } 60 | 61 | var body: some View { 62 | VStack { 63 | ZStack { 64 | viewBody 65 | } 66 | .popup(isPresented: $featuredUserStore.followManyFailed) { 67 | Toast(text: "Failed to follow users", type: .error) 68 | } customize: { 69 | $0.type(.toast).autohideIn(2) 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Onboarding/Models/SelectedCity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectedCity.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum SelectedCity: Equatable { 11 | case nyc, la, chicago, london, other 12 | 13 | var name: String { 14 | switch self { 15 | case .nyc: return "New York" 16 | case .la: return "Los Angeles" 17 | case .chicago: return "Chicago" 18 | case .london: return "London" 19 | case .other: return "Other" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Onboarding/Models/SuggestedUserStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SuggestedUserStore.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/14/21. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol SuggestedUserStore: ObservableObject { 11 | var allUsers: [PublicUser] { get } 12 | var selectedUsernames: Set { get } 13 | var followingLoading: Bool { get } 14 | 15 | func follow(appState: AppState) 16 | func toggleSelected(for username: String) 17 | func clearAll() 18 | func selectAll() 19 | } 20 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Onboarding/OnboardingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingView.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/8/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OnboardingView: View { 11 | @EnvironmentObject var appState: AppState 12 | @EnvironmentObject var onboardingModel: OnboardingModel 13 | @StateObject var navigationState = NavigationState() 14 | 15 | @State var city: String? 16 | @State var showSubmitPlacesWarning = false 17 | 18 | var body: some View { 19 | Navigator(state: navigationState) { 20 | VStack { 21 | switch onboardingModel.onboardingStep { 22 | case .requestLocation: 23 | RequestLocation(onCompleteRequest: onboardingModel.step) 24 | case .followFeatured: 25 | FollowFeatured(onboardingModel: onboardingModel) 26 | case .cityOnboarding: 27 | CityOnboarding(selectCity: { city in 28 | Analytics.track(.onboardingCitySelected, parameters: ["city": city.name]) 29 | DispatchQueue.main.async { 30 | if city == .other { 31 | onboardingModel.step() 32 | } else { 33 | navigationState.push(.cityOnboarding(city: city.name)) 34 | } 35 | } 36 | }) 37 | case .completed: 38 | EmptyView() 39 | } 40 | } 41 | .navigationBarTitleDisplayMode(.inline) 42 | .navigationBarHidden(true) 43 | .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))) 44 | .trackScreen(.onboarding) 45 | } 46 | .environmentObject(onboardingModel) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Onboarding/RequestLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestLocation.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/28/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RequestLocation: View { 11 | var onCompleteRequest: () -> Void 12 | 13 | var body: some View { 14 | RequestPermission( 15 | onCompleteRequest: onCompleteRequest, 16 | action: PermissionManager.shared.requestLocation, 17 | title: "Jimo works best with your location enabled", 18 | imageName: "location-icon", 19 | caption: "View your location on the Jimo map." 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Onboarding/RequestPermission.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestPermission.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 3/27/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RequestPermission: View { 11 | @State private var requesting = false 12 | 13 | var onCompleteRequest: () -> Void 14 | 15 | var action: () -> Void 16 | var title: String 17 | var imageName: String 18 | var caption: String 19 | var privacyCaption: String = "" 20 | 21 | func request() { 22 | action() 23 | withAnimation { 24 | self.requesting = true 25 | } 26 | } 27 | 28 | func next() { 29 | withAnimation { 30 | self.onCompleteRequest() 31 | } 32 | } 33 | 34 | var body: some View { 35 | VStack(spacing: 20) { 36 | Text(title) 37 | .font(.system(size: 24)) 38 | .multilineTextAlignment(.center) 39 | .padding(40) 40 | 41 | Group { 42 | Image(imageName) 43 | 44 | Text(caption) 45 | } 46 | 47 | Spacer() 48 | 49 | VStack(spacing: 20) { 50 | if !requesting { 51 | Button(action: { 52 | request() 53 | }) { 54 | LargeButton("Continue", fontSize: 20) 55 | } 56 | } else { 57 | Button(action: { 58 | next() 59 | }) { 60 | Text("Next") 61 | .font(.system(size: 20)) 62 | .frame(minWidth: 0, maxWidth: .infinity) 63 | .frame(height: 60) 64 | .foregroundColor(.white) 65 | .background(Color("next-button")) 66 | .cornerRadius(10) 67 | .shadow(radius: 5) 68 | } 69 | } 70 | }.padding(.horizontal, 80) 71 | 72 | Text(privacyCaption) 73 | .font(.caption) 74 | .padding(.horizontal, 50) 75 | } 76 | .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))) 77 | .foregroundColor(Color("foreground")) 78 | .padding(.bottom, 100) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Onboarding/UnauthedOnboarding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnauthedOnboarding.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/1/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UnauthedOnboarding: View { 11 | @EnvironmentObject var appState: AppState 12 | @StateObject var viewModel = ViewModel() 13 | 14 | var body: some View { 15 | if !viewModel.onboarded { 16 | VStack { 17 | HStack { 18 | Button { 19 | appState.signOut() 20 | } label: { 21 | Text("Back") 22 | } 23 | Spacer() 24 | }.padding() 25 | 26 | RequestLocation(onCompleteRequest: { 27 | DispatchQueue.main.async { 28 | appState.onboardingModel.skipLocationIfGranted() 29 | viewModel.onboarded = true 30 | } 31 | }) 32 | .trackScreen(.guestLocationOnboarding) 33 | } 34 | } else { 35 | MainAppView(notificationsModel: appState.notificationsModel, currentUser: nil) 36 | } 37 | } 38 | } 39 | 40 | extension UnauthedOnboarding { 41 | class ViewModel: ObservableObject { 42 | @Published var onboarded: Bool 43 | 44 | init() { 45 | self.onboarded = PermissionManager.shared.locationManager.location != nil 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Post/Create/CreatePostCategoryPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreatePostCategoryPicker.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/6/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CreatePostCategoryPicker: View { 11 | @Binding var category: String? 12 | 13 | var body: some View { 14 | VStack { 15 | HStack { 16 | Text("Select category") 17 | .font(.system(size: 15)) 18 | .bold() 19 | Spacer() 20 | } 21 | 22 | ScrollView(.horizontal, showsIndicators: false) { 23 | HStack { 24 | CreatePostCategory(name: "Food", key: "food", selected: $category) 25 | CreatePostCategory(name: "Cafe", key: "cafe", selected: $category) 26 | CreatePostCategory(name: "Nightlife", key: "nightlife", selected: $category) 27 | CreatePostCategory(name: "Activity", key: "activity", selected: $category) 28 | CreatePostCategory(name: "Shopping", key: "shopping", selected: $category) 29 | CreatePostCategory(name: "Lodging", key: "lodging", selected: $category) 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | private struct CreatePostCategory: View { 37 | var name: String 38 | var key: String 39 | @Binding var selected: String? 40 | 41 | var colored: Bool { 42 | selected == nil || selected == key 43 | } 44 | 45 | var body: some View { 46 | VStack { 47 | Image(key) 48 | .resizable() 49 | .scaledToFit() 50 | .frame(height: 30) 51 | 52 | Text(name) 53 | .font(.system(size: 10)) 54 | .foregroundColor(.black) 55 | } 56 | .padding(.vertical, 7.5) 57 | .frame(width: 60, height: 60) 58 | .background(colored ? Color(key) : Color("unselected")) 59 | .cornerRadius(10) 60 | .onTapGesture { 61 | self.selected = self.selected == key ? nil : key 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Post/Create/ImageSelectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageSelectionView.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 2/6/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ImageSelectionView: View { 11 | @ObservedObject var createPostVM: CreatePostVM 12 | 13 | func imageView(image: CreatePostImage) -> some View { 14 | Group { 15 | switch image { 16 | case .uiImage(let image): 17 | Image(uiImage: image) 18 | .resizable() 19 | .scaledToFill() 20 | .frame(width: 100, height: 100) 21 | case .webImage(let item): 22 | URLImage(url: item.url) 23 | .scaledToFill() 24 | .frame(width: 100, height: 100) 25 | } 26 | } 27 | } 28 | 29 | var body: some View { 30 | Group { 31 | HStack(spacing: 10) { 32 | ForEach(createPostVM.images, id: \.self) { image in 33 | ZStack(alignment: .topLeading) { 34 | imageView(image: image) 35 | .frame(width: 100, height: 100) 36 | .cornerRadius(2) 37 | Button { 38 | createPostVM.removeImage(image) 39 | } label: { 40 | Image(systemName: "xmark.circle.fill") 41 | .resizable() 42 | .frame(width: 25, height: 25) 43 | .foregroundColor(Color(white: 0.9)) 44 | .background(Color.black) 45 | .cornerRadius(13) 46 | .padding(5) 47 | .contentShape(Rectangle()) 48 | } 49 | } 50 | } 51 | 52 | if createPostVM.images.count < 3 { 53 | ZStack { 54 | Rectangle() 55 | .fill(Color.gray.opacity(0.2)) 56 | .onTapGesture { 57 | createPostVM.activeSheet = .imagePicker 58 | } 59 | 60 | Image(systemName: "plus") 61 | .resizable() 62 | .scaledToFit() 63 | .frame(width: 30) 64 | .foregroundColor(Color.gray.opacity(0.5)) 65 | } 66 | .frame(width: 100, height: 100) 67 | .cornerRadius(2) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Post/Create/LocationSearch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationSearch.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 11/10/20. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import MapKit 11 | import Combine 12 | 13 | enum SearchState: String { 14 | case autocomplete, search 15 | } 16 | 17 | // From https://www.mozzafiller.com/posts/mklocalsearchcompleter-swiftui-combine 18 | class LocationSearch: NSObject, ObservableObject, MKLocalSearchCompleterDelegate { 19 | @Published var searchQuery = "" { 20 | didSet { 21 | if searchQuery != oldValue { 22 | DispatchQueue.main.async { 23 | self.searchState = .autocomplete 24 | self.mkSearchResults.removeAll() 25 | } 26 | } 27 | } 28 | } 29 | @Published var completions: [MKLocalSearchCompletion] = [] 30 | @Published var startedSearching = false 31 | @Published var mkSearchResults: [MKMapItem] = [] 32 | 33 | @Published var searchState: SearchState = .autocomplete 34 | 35 | var completer: MKLocalSearchCompleter 36 | var cancellable: AnyCancellable? 37 | 38 | override init() { 39 | self.completer = MKLocalSearchCompleter() 40 | super.init() 41 | completer.resultTypes = [.address, .pointOfInterest] 42 | completer.delegate = self 43 | cancellable = $searchQuery.assign(to: \.queryFragment, on: self.completer) 44 | } 45 | 46 | func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { 47 | self.completions = completer.results 48 | self.startedSearching = true 49 | } 50 | 51 | func search() { 52 | self.searchState = .search 53 | let request = MKLocalSearch.Request() 54 | request.naturalLanguageQuery = searchQuery 55 | let search = MKLocalSearch(request: request) 56 | search.start { (response, _) in 57 | let places = response?.mapItems 58 | if let places = places { 59 | self.mkSearchResults = places 60 | } 61 | } 62 | } 63 | } 64 | 65 | extension MKLocalSearchCompletion: Identifiable {} 66 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Post/Create/MultilineTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultilineTextField.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 11/6/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MultilineTextField: View { 11 | @Environment(\.colorScheme) var colorScheme 12 | @Binding private var text: String 13 | 14 | private var placeholder: String 15 | private var height: CGFloat = 80 16 | var showingPlaceholder: Bool { 17 | text.isEmpty 18 | } 19 | 20 | init (_ placeholder: String = "", text: Binding, height: CGFloat? = nil) { 21 | self.placeholder = placeholder 22 | self._text = text 23 | self.height = height ?? self.height 24 | } 25 | 26 | var body: some View { 27 | TextEditor(text: $text) 28 | .textFieldStyle(.plain) 29 | .frame(minHeight: height, maxHeight: height) 30 | .overlay(placeholderView, alignment: .topLeading) 31 | } 32 | 33 | @ViewBuilder 34 | var placeholderView: some View { 35 | if showingPlaceholder { 36 | Text(placeholder) 37 | .foregroundColor(.gray) 38 | .padding(.leading, 4) 39 | .padding(.top, 8) 40 | .allowsHitTesting(false) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Jimo/Jimo/Views/Post/PostGridCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostGridCell.swift 3 | // Jimo 4 | // 5 | // Created by Gautam Mekkat on 5/11/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PostGridCell: View { 11 | var post: Post 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: 0) { 15 | GeometryReader { geometry in 16 | if let url = post.imageUrl { 17 | URLImage(url: url, thumbnail: true) 18 | .frame(maxWidth: .infinity) 19 | .frame(width: geometry.size.width, height: geometry.size.width) 20 | } else { 21 | MapSnapshotView(post: post, width: (UIScreen.main.bounds.width - 6) / 3) 22 | .frame(maxWidth: .infinity) 23 | .frame(height: geometry.size.width) 24 | } 25 | } 26 | .aspectRatio(1, contentMode: .fit) 27 | .background(Color(post.category)) 28 | .cornerRadius(2) 29 | .padding(.bottom, 5) 30 | 31 | VStack(alignment: .leading, spacing: 0) { 32 | caption 33 | Spacer() 34 | }.frame(height: 60) 35 | } 36 | } 37 | 38 | @ViewBuilder 39 | var caption: some View { 40 | if let stars = post.stars { 41 | HStack(spacing: 1) { 42 | if stars == 0 { 43 | Image(systemName: "star.slash.fill") 44 | .foregroundColor(.gray) 45 | } else { 46 | ForEach(0.. Bool { 95 | lhs.id == rhs.id 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Jimo/JimoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Jimo/JimoTests/JimoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JimoTests.swift 3 | // JimoTests 4 | // 5 | // Created by Gautam Mekkat on 11/6/20. 6 | // 7 | 8 | import XCTest 9 | @testable import Jimo 10 | 11 | class JimoTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Jimo/JimoUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Jimo/JimoUITests/JimoUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JimoUITests.swift 3 | // JimoUITests 4 | // 5 | // Created by Gautam Mekkat on 11/6/20. 6 | // 7 | 8 | import XCTest 9 | 10 | class JimoUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | --------------------------------------------------------------------------------