├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
4 |
--------------------------------------------------------------------------------
/Jimo/Jimo/Assets.xcassets/permissions/contacts-icon.imageset/contacts-icon.svg:
--------------------------------------------------------------------------------
1 |
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 |
5 |
--------------------------------------------------------------------------------
/Jimo/Jimo/Assets.xcassets/permissions/location-icon.imageset/location-icon.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------