├── .github
└── workflows
│ ├── ci.yml
│ └── stale.yml
├── .gitignore
├── .swiftlint.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SnapCat.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ └── swiftpm
│ └── Package.resolved
├── SnapCat
├── Activity
│ ├── ActivityView.swift
│ ├── ActivityViewModel.swift
│ ├── EmptyActivityView.swift
│ └── EmptyActivityViewController.swift
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── 120-1.png
│ │ ├── 120.png
│ │ ├── 152.png
│ │ ├── 167.png
│ │ ├── 180.png
│ │ ├── 20.png
│ │ ├── 29.png
│ │ ├── 40-1.png
│ │ ├── 40-2.png
│ │ ├── 40.png
│ │ ├── 58-1.png
│ │ ├── 58.png
│ │ ├── 60.png
│ │ ├── 76.png
│ │ ├── 80-1.png
│ │ ├── 80.png
│ │ ├── 87.png
│ │ ├── Contents.json
│ │ └── appstore.png
│ ├── Contents.json
│ ├── Snapcat.imageset
│ │ ├── 1024.png
│ │ ├── 256.png
│ │ ├── 512.png
│ │ └── Contents.json
│ └── netrecon.imageset
│ │ ├── Contents.json
│ │ ├── netrecon-1.png
│ │ ├── netrecon-2.png
│ │ └── netrecon.png
├── Constants.swift
├── CustomViewModels
│ ├── CloudImageViewModel.swift
│ └── QueryImageViewModel.swift
├── Environment
│ └── TintColorKey.swift
├── Explore
│ ├── EmptyExploreView.swift
│ ├── EmptyExploreViewController.swift
│ ├── ExploreView.swift
│ ├── ExploreViewModel.swift
│ └── SearchBarView.swift
├── Extentions
│ ├── Date+extension.swift
│ ├── Logger+extension.swift
│ ├── ParseSwift+extention.swift
│ ├── UIImage+extension.swift
│ └── View+extension.swift
├── Home
│ ├── CommentView.swift
│ ├── CommentViewModel.swift
│ ├── EmptyTimeLineView.swift
│ ├── EmptyTimeLineViewController.swift
│ ├── HomeView.swift
│ ├── HomeViewModel.swift
│ ├── ImagePickerView.swift
│ ├── PostView.swift
│ ├── PostViewModel.swift
│ ├── TimeLineCommentsView.swift
│ ├── TimeLineLikeCommentView.swift
│ ├── TimeLinePostView.swift
│ ├── TimeLineView.swift
│ ├── TimeLineViewModel.swift
│ └── ViewAllComments.swift
├── Info.plist
├── Main
│ ├── EmptyDefaultViewController.swift
│ └── MainView.swift
├── Models
│ ├── Activity.swift
│ ├── Installation.swift
│ ├── Post.swift
│ └── User.swift
├── Onboarding
│ ├── OnboardingView.swift
│ └── OnboardingViewModel.swift
├── ParseSwift.plist
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Profile
│ ├── ProfileEditView.swift
│ ├── ProfileHeaderView.swift
│ ├── ProfileUserDetailsView.swift
│ ├── ProfileView.swift
│ ├── ProfileViewModel.swift
│ ├── SettingsView.swift
│ └── SettingsViewModel.swift
├── SnapCat.entitlements
├── SnapCatApp.swift
├── SnapCatError.swift
├── Utility.swift
└── ViewModels
│ ├── PostStatus.swift
│ └── UserStatus.swift
├── SnapCatTests
├── Info.plist
└── SnapCatTests.swift
└── SnapCatUITests
├── Info.plist
└── SnapCatUITests.swift
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 | on:
3 | push:
4 | branches: [ main ]
5 | pull_request:
6 | branches: [ main ]
7 |
8 | env:
9 | CI_XCODE: '/Applications/Xcode_14.2.app/Contents/Developer'
10 |
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.ref }}
13 | cancel-in-progress: true
14 |
15 | jobs:
16 | ios:
17 | runs-on: macos-latest
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Build-Test
21 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project SnapCat.xcodeproj -scheme SnapCat -destination platform\=iOS\ Simulator,name\=iPhone\ 12\ Pro\ Max build | xcpretty
22 | env:
23 | DEVELOPER_DIR: ${{ env.CI_XCODE }}
24 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: Mark stale issues and pull requests
2 |
3 | on:
4 | schedule:
5 | - cron: "30 1 * * *"
6 |
7 | jobs:
8 | stale:
9 |
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/stale@v1
14 | with:
15 | repo-token: ${{ secrets.GITHUB_TOKEN }}
16 | stale-issue-message: 'Stale issue message'
17 | stale-pr-message: 'Stale pull request message'
18 | stale-issue-label: 'no-issue-activity'
19 | stale-pr-label: 'no-pr-activity'
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | .DS_Store
11 | *.pbxuser
12 | !default.pbxuser
13 | *.mode1v3
14 | !default.mode1v3
15 | *.mode2v3
16 | !default.mode2v3
17 | *.perspectivev3
18 | !default.perspectivev3
19 | xcuserdata/
20 |
21 | ## Other
22 | *.moved-aside
23 | *.xcuserstate
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | .build/
40 |
41 | # CocoaPods
42 | #
43 | # We recommend against adding the Pods directory to your .gitignore. However
44 | # you should judge for yourself, the pros and cons are mentioned at:
45 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
46 | #
47 | # Pods/
48 |
49 | # Carthage
50 | #
51 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
52 | # Carthage/Checkouts
53 |
54 | Carthage/Build
55 |
56 | # fastlane
57 | #
58 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
59 | # screenshots whenever they are needed.
60 | # For more information about the recommended setup visit:
61 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
62 |
63 | fastlane/report.xml
64 | fastlane/Preview.html
65 | fastlane/screenshots
66 | fastlane/test_output
67 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | excluded: # paths to ignore during linting. Takes precedence over `included`.
2 | - DerivedData
3 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to SnapCat
2 |
3 | If you are not familiar with Pull Requests and want to know more about them, you can visit the [Creating a pull request](https://help.github.com/articles/creating-a-pull-request/) article. It contains detailed information about the process.
4 |
5 | ## Setting up your local machine
6 |
7 | * [Fork](https://github.com/netreconlab/SnapCat.git) this project and clone the fork on to your local machine:
8 |
9 | ```sh
10 | $ git clone https://github.com/netreconlab/SnapCat.git
11 | $ cd ParseCareKit # go into the clone directory
12 | ```
13 |
14 | * Please install [SwiftLint](https://github.com/realm/SwiftLint) to ensure that your PR conforms to our coding standards:
15 |
16 | ```sh
17 | $ brew install swiftlint
18 | ```
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Network Reconnaissance Lab
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SnapCat
2 |    
3 |
4 |
5 |
6 | SnapCat is a social media application for posting pictures, comments, and finding friends. SnapCat is designed using SwiftUI and the [ParseSwift SDK](https://github.com/netreconlab/Parse-Swift). The app is meant to serve as a base app for University of Kentucky graudate researchers and undergraduate students learning iOS mobile app development.
7 |
8 | ## Setup Your Parse Server
9 | You can setup your parse-server locally to test using [snapcat branch](https://github.com/netreconlab/parse-hipaa/tree/snapcat) of [parse-hipaa](https://github.com/netreconlab/parse-hipaa). Simply type the following to get your parse-server running with postgres locally:
10 |
11 | 1. Fork [parse-hipaa](https://github.com/netreconlab/parse-hipaa/tree/snapcat)
12 | 2. `cd parse-hipaa`
13 | 3. `docker-compose up` - this will take a couple of minutes to setup as it needs to initialize postgres, but as soon as you see `parse-server running on port 1337.`, it's ready to go. See [here](https://github.com/netreconlab/parse-hipaa#getting-started) for details
14 | 4. If you would like to use mongo instead of postgres, in step 3, type `docker-compose -f docker-compose.mongo.yml up` instead of `docker-compose up`
15 |
16 | ## Fork this repo
17 |
18 | 1. Fork [SnapCat](https://github.com/netreconlab/SnapCat.git), in particular the [snapcat branch](https://github.com/netreconlab/parse-hipaa/tree/snapcat).
19 | 2. Open `SnapCat.xcodeproj` in Xcode
20 | 3. You may need to configure your "Team" and "Bundle Identifier" in "Signing and Capabilities"
21 | 4. Run the app and data will synchronize with parse-hipaa via http://localhost:1337/parse automatically
22 | 5. You can edit Parse server setup in the ParseSwift.plist file in the Xcode browser
23 |
24 | ## View your data in Parse Dashboard
25 | Parse Dashboard is the easiest way to view your data in the Cloud (or local machine in this example) and comes with [parse-hipaa](https://github.com/netreconlab/parse-hipaa). To access:
26 | 1. Open your browser and go to http://localhost:4040/dashboard
27 | 2. Username: `parse`
28 | 3. Password: `1234`
29 | 4. Be sure to refresh your browser to see new changes synched from your CareKitSample app
30 |
--------------------------------------------------------------------------------
/SnapCat.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SnapCat.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SnapCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "dznemptydataset",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/dzenbot/DZNEmptyDataSet.git",
7 | "state" : {
8 | "branch" : "master",
9 | "revision" : "9bffa69a83a9fa58a14b3cf43cb6dd8a63774179"
10 | }
11 | },
12 | {
13 | "identity" : "parse-swift",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/netreconlab/Parse-Swift.git",
16 | "state" : {
17 | "revision" : "a5ba8cb16651ea189b888981e98c2606dc73573d",
18 | "version" : "4.16.2"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/SnapCat/Activity/ActivityView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivityView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ActivityView: View {
12 | @ObservedObject var followingsActivityViewModel = ActivityViewModel
13 | .queryFollowingsActivity
14 | .include(ActivityKey.fromUser)
15 | .include(ActivityKey.toUser)
16 | .viewModel
17 |
18 | var body: some View {
19 | if !followingsActivityViewModel.results.isEmpty {
20 | List(followingsActivityViewModel.results, id: \.id) { result in
21 | VStack {
22 | HStack {
23 | if let fromUsername = result.fromUser?.username {
24 | Text("@\(fromUsername)")
25 | .font(.headline)
26 | }
27 | if let activity = result.type {
28 | switch activity {
29 | case .like:
30 | Text("liked")
31 | case .follow:
32 | Text("followed")
33 | case .comment:
34 | Text("commented on")
35 | }
36 | }
37 | if let fromUsername = result.toUser?.username {
38 | Text("@\(fromUsername)")
39 | .font(.headline)
40 | }
41 | Spacer()
42 | }
43 | if let createdAt = result.createdAt {
44 | HStack {
45 | Text(createdAt.relativeTime)
46 | .font(.footnote)
47 | Spacer()
48 | }
49 | }
50 | }
51 | }
52 | .onAppear(perform: {
53 | followingsActivityViewModel.find()
54 | })
55 | } else {
56 | EmptyActivityView()
57 | }
58 | }
59 |
60 | init () {
61 | followingsActivityViewModel.find()
62 | }
63 | }
64 |
65 | struct ActivityView_Previews: PreviewProvider {
66 | static var previews: some View {
67 | ActivityView()
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/SnapCat/Activity/ActivityViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivityViewModel.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 | import ParseSwift
12 |
13 | class ActivityViewModel: ObservableObject {
14 |
15 | class var queryFollowingsActivity: Query {
16 | let followings = ProfileViewModel.queryFollowings()
17 | return Activity.query(matchesKeyInQuery(key: ActivityKey.fromUser,
18 | queryKey: ActivityKey.toUser,
19 | query: followings))
20 | .order([.descending(ParseKey.createdAt)])
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/SnapCat/Activity/EmptyActivityView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyActivityView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import UIKit
11 |
12 | struct EmptyActivityView: UIViewControllerRepresentable {
13 |
14 | func makeUIViewController(context: Context) -> some UIViewController {
15 | let view = EmptyActivityViewController()
16 | let viewController = UINavigationController(rootViewController: view)
17 | viewController.navigationBar.isHidden = true
18 | return viewController
19 | }
20 |
21 | func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
22 |
23 | }
24 | }
25 |
26 | struct EmptyActivityView_Previews: PreviewProvider {
27 | static var previews: some View {
28 | EmptyActivityView()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SnapCat/Activity/EmptyActivityViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyActivityViewController.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import DZNEmptyDataSet
12 |
13 | class EmptyActivityViewController: EmptyDefaultViewController {
14 |
15 | override func viewDidLoad() {
16 | super.viewDidLoad()
17 | }
18 |
19 | override func viewDidAppear(_ animated: Bool) {
20 | super.viewDidAppear(animated)
21 | }
22 |
23 | override func title(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString {
24 | let text = "Activity"
25 |
26 | let attributes = [
27 | NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 18.0),
28 | NSAttributedString.Key.foregroundColor: UIColor.darkGray
29 | ]
30 |
31 | return NSAttributedString(string: text, attributes: attributes)
32 | }
33 |
34 | override func description(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString {
35 | let text = "See acitivity from people you follow"
36 |
37 | let paragragh = NSMutableParagraphStyle()
38 | paragragh.lineBreakMode = NSLineBreakMode.byWordWrapping
39 | paragragh.alignment = NSTextAlignment.center
40 |
41 | let attributes = [
42 | NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 14.0),
43 | NSAttributedString.Key.foregroundColor: UIColor.lightGray,
44 | NSAttributedString.Key.paragraphStyle: paragragh
45 | ]
46 |
47 | return NSAttributedString(string: text, attributes: attributes)
48 | }
49 |
50 | override func buttonTitle(forEmptyDataSet scrollView: UIScrollView,
51 | for state: UIControl.State) -> NSAttributedString {
52 | let attributes = [
53 | NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 17.0)
54 | ]
55 |
56 | return NSAttributedString(string: "Tap here to find friends", attributes: attributes)
57 | }
58 |
59 | override func image(forEmptyDataSet scrollView: UIScrollView) -> UIImage? {
60 |
61 | if var image = UIImage(systemName: "heart") {
62 |
63 | image = image.tint(UIColor.lightGray, blendMode: CGBlendMode.color)
64 | image = image.imageRotatedByDegrees(180, flip: false)
65 | return image
66 | }
67 | return UIImage()
68 |
69 | }
70 |
71 | override func emptyDataSet(_ scrollView: UIScrollView, didTap view: UIView) {
72 | presentView()
73 | }
74 |
75 | override func emptyDataSet(_ scrollView: UIScrollView, didTap button: UIButton) {
76 | presentView()
77 | }
78 |
79 | func presentView() {
80 | let friendsViewController = ExploreView().formattedHostingController()
81 | friendsViewController.modalPresentationStyle = .popover
82 | present(friendsViewController, animated: true, completion: nil)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/SnapCat/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 |
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/120-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/120-1.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/40-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/40-1.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/40-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/40-2.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/58-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/58-1.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/76.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/80-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/80-1.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "40.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "60.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "58.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "87.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "80.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "120.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "120-1.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "180.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "20.png",
53 | "idiom" : "ipad",
54 | "scale" : "1x",
55 | "size" : "20x20"
56 | },
57 | {
58 | "filename" : "40-1.png",
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "20x20"
62 | },
63 | {
64 | "filename" : "29.png",
65 | "idiom" : "ipad",
66 | "scale" : "1x",
67 | "size" : "29x29"
68 | },
69 | {
70 | "filename" : "58-1.png",
71 | "idiom" : "ipad",
72 | "scale" : "2x",
73 | "size" : "29x29"
74 | },
75 | {
76 | "filename" : "40-2.png",
77 | "idiom" : "ipad",
78 | "scale" : "1x",
79 | "size" : "40x40"
80 | },
81 | {
82 | "filename" : "80-1.png",
83 | "idiom" : "ipad",
84 | "scale" : "2x",
85 | "size" : "40x40"
86 | },
87 | {
88 | "filename" : "76.png",
89 | "idiom" : "ipad",
90 | "scale" : "1x",
91 | "size" : "76x76"
92 | },
93 | {
94 | "filename" : "152.png",
95 | "idiom" : "ipad",
96 | "scale" : "2x",
97 | "size" : "76x76"
98 | },
99 | {
100 | "filename" : "167.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "83.5x83.5"
104 | },
105 | {
106 | "filename" : "appstore.png",
107 | "idiom" : "ios-marketing",
108 | "scale" : "1x",
109 | "size" : "1024x1024"
110 | }
111 | ],
112 | "info" : {
113 | "author" : "xcode",
114 | "version" : 1
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/AppIcon.appiconset/appstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/AppIcon.appiconset/appstore.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/Snapcat.imageset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/Snapcat.imageset/1024.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/Snapcat.imageset/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/Snapcat.imageset/256.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/Snapcat.imageset/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/Snapcat.imageset/512.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/Snapcat.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "256.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "512.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "1024.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/netrecon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "netrecon.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "netrecon-1.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "netrecon-2.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/netrecon.imageset/netrecon-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/netrecon.imageset/netrecon-1.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/netrecon.imageset/netrecon-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/netrecon.imageset/netrecon-2.png
--------------------------------------------------------------------------------
/SnapCat/Assets.xcassets/netrecon.imageset/netrecon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/SnapCat/3098fcbe474a8e623c227d58f4883df9de334718/SnapCat/Assets.xcassets/netrecon.imageset/netrecon.png
--------------------------------------------------------------------------------
/SnapCat/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/3/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum Constants {
12 | static let firstRun = "FirstRun"
13 | static let lastProfilePicURL = "lastProfilePicURL"
14 | static let numberOfImagesToDownload = 50
15 | static let fileDownloadsDirectory = "Downloads"
16 | }
17 |
18 | // MARK: - Standard Parse Class Keys
19 | enum ParseKey {
20 | static let objectId = "objectId"
21 | static let createdAt = "createdAt"
22 | static let updatedAt = "updatedAt"
23 | static let ACL = "ACL"
24 | }
25 |
26 | // MARK: - User Class Keys
27 | enum UserKey {
28 | static let username = "username"
29 | static let password = "password"
30 | static let email = "email"
31 | static let emailVerified = "emailVerified"
32 | static let authData = "authData"
33 | static let name = "name"
34 | static let profileImage = "profileImage"
35 | static let profileThumbnail = "profileThumbnail"
36 | static let bio = "bio"
37 | static let link = "link"
38 | }
39 |
40 | // MARK: - Installation Class Keys
41 | enum InstallationKey {
42 | static let deviceType = "deviceType"
43 | static let installationId = "installationId"
44 | static let deviceToken = "deviceToken"
45 | static let badge = "badge"
46 | static let timeZone = "timeZone"
47 | static let channels = "channels"
48 | static let appName = "appName"
49 | static let appIdentifier = "appIdentifier"
50 | static let parseVersion = "parseVersion"
51 | static let localeIdentifier = "localeIdentifier"
52 | }
53 |
54 | // MARK: - Activity Class Keys
55 | enum ActivityKey {
56 | static let fromUser = "fromUser"
57 | static let toUser = "toUser"
58 | static let type = "type"
59 | static let comment = "comment"
60 | static let post = "post"
61 | static let activity = "activity"
62 | }
63 |
64 | // MARK: - Post Class Keys
65 | enum PostKey {
66 | static let user = "user"
67 | static let image = "image"
68 | static let thumbnail = "thumbnail"
69 | static let location = "location"
70 | static let caption = "caption"
71 | }
72 |
--------------------------------------------------------------------------------
/SnapCat/CustomViewModels/CloudImageViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CloudImageViewModel.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/12/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 |
12 | class CloudImageViewModel: CloudViewModel {
13 |
14 | override var results: T.ReturnType? {
15 | willSet {
16 |
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/SnapCat/CustomViewModels/QueryImageViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QueryImageViewModel.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/12/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 | import ParseSwift
12 | import UIKit
13 |
14 | class QueryImageViewModel: Subscription {
15 |
16 | override var results: [QueryViewModel.Object] {
17 | willSet {
18 | count = newValue.count
19 | if let posts = newValue as? [Post] {
20 | postResults = posts
21 | } else if let users = newValue as? [User] {
22 | userResults = users
23 | }
24 | DispatchQueue.main.async {
25 | self.objectWillChange.send()
26 | }
27 | }
28 | }
29 |
30 | override var event: (query: Query, event: Event)? {
31 | willSet {
32 | guard let event = newValue?.event else {
33 | return
34 | }
35 | switch event {
36 |
37 | case .created(let object), .entered(let object):
38 | if var post = object as? Post {
39 | // LiveQuery doesn't include pointers, need to check to fetch
40 | guard let userObjectId = post.user?.id,
41 | let user = relatedUser[userObjectId] else {
42 | let currentPost = post
43 | Task {
44 | do {
45 | let fetchPost = try await currentPost.fetch(includeKeys: [PostKey.user])
46 | if let parseObject = fetchPost as? T {
47 | self.results.insert(parseObject, at: 0)
48 | }
49 | } catch {
50 | Logger.post.error("Couldn't fetch \(error.localizedDescription)")
51 | }
52 | }
53 | return
54 | }
55 | post.user = user
56 | if let parseObject = post as? T {
57 | self.results.insert(parseObject, at: 0)
58 | }
59 | }
60 |
61 | case .updated(let object):
62 | guard let index = results.firstIndex(where: { $0.hasSameObjectId(as: object) }) else {
63 | return
64 | }
65 | if var post = object as? Post {
66 | // LiveQuery doesn't include pointers, need to check to fetch
67 | guard let userObjectId = post.user?.id,
68 | let user = relatedUser[userObjectId] else {
69 | let currentPost = post
70 | Task {
71 | do {
72 | let fetchPost = try await currentPost
73 | .fetch(includeKeys: [PostKey.user])
74 | if let parseObject = fetchPost as? T {
75 | self.results[index] = parseObject
76 | }
77 | } catch {
78 | Logger.post.error("Couldn't fetch \(error.localizedDescription)")
79 | }
80 | }
81 | return
82 | }
83 | post.user = user
84 | if let parseObject = post as? T {
85 | self.results[index] = parseObject
86 | }
87 | }
88 |
89 | case .deleted(let object):
90 | guard let index = results.firstIndex(where: { $0.hasSameObjectId(as: object) }) else {
91 | return
92 | }
93 | results.remove(at: index)
94 | default:
95 | break
96 | }
97 | subscribed = nil
98 | unsubscribed = nil
99 | }
100 | }
101 |
102 | var userOfInterest: User?
103 |
104 | var postResults = [Post]() {
105 | willSet {
106 | newValue.forEach { object in
107 | storeRelatedUser(object.user)
108 | if likes[object.id] == nil {
109 | Task {
110 | do {
111 | let foundLikes = try await PostViewModel
112 | .queryLikes(post: object)
113 | .find()
114 | DispatchQueue.main.async {
115 | self.likes[object.id] = foundLikes
116 | }
117 | } catch {
118 | Logger.post.error("QueryImageViewModel: couldn't find likes: \(error.localizedDescription)")
119 | }
120 | }
121 | }
122 | if comments[object.id] == nil {
123 | Task {
124 | do {
125 | let foundComments = try await PostViewModel
126 | .queryComments(post: object)
127 | .include(ActivityKey.fromUser)
128 | .find()
129 | DispatchQueue.main.async {
130 | self.comments[object.id] = foundComments
131 | }
132 | } catch {
133 | // swiftlint:disable:next line_length
134 | Logger.post.error("QueryImageViewModel: couldn't find comments: \(error.localizedDescription)")
135 | }
136 | }
137 | }
138 | // Fetch images
139 | if imageResults.count >= Constants.numberOfImagesToDownload {
140 | return
141 | }
142 | guard imageResults[object.id] == nil else {
143 | return
144 | }
145 | Task {
146 | if let image = await Utility.fetchImage(object.image) {
147 | DispatchQueue.main.async {
148 | self.imageResults[object.id] = image
149 | }
150 | }
151 | }
152 | Task {
153 | if let image = await Utility.fetchImage(object.user?.profileImage) {
154 | if let userObjectId = object.user?.id {
155 | DispatchQueue.main.async {
156 | self.imageResults[userObjectId] = image
157 | }
158 | }
159 | }
160 |
161 | }
162 | }
163 | DispatchQueue.main.async {
164 | self.objectWillChange.send()
165 | }
166 | }
167 | }
168 |
169 | var userResults = [User]() {
170 | willSet {
171 | newValue.forEach { object in
172 | storeRelatedUser(object)
173 | // Fetch images
174 | if imageResults.count == Constants.numberOfImagesToDownload {
175 | return
176 | }
177 | guard imageResults[object.id] == nil else {
178 | return
179 | }
180 | Task {
181 | if let image = await Utility.fetchImage(object.profileImage) {
182 | DispatchQueue.main.async {
183 | self.imageResults[object.id] = image
184 | }
185 | }
186 | }
187 | }
188 | }
189 | }
190 |
191 | /// Contains all fetched images.
192 | var imageResults = [String: UIImage]() {
193 | willSet {
194 | DispatchQueue.main.async {
195 | self.objectWillChange.send()
196 | }
197 | }
198 | }
199 |
200 | /// Contains all fetched thumbnail images.
201 | var thubmNailImageResults = [String: UIImage]() {
202 | willSet {
203 | DispatchQueue.main.async {
204 | self.objectWillChange.send()
205 | }
206 | }
207 | }
208 |
209 | /// Contains likes for each post
210 | var likes = [String: [Activity]]() {
211 | willSet {
212 | DispatchQueue.main.async {
213 | self.objectWillChange.send()
214 | }
215 | }
216 | }
217 |
218 | /// Contains comments for each post
219 | var comments = [String: [Activity]]() {
220 | willSet {
221 | DispatchQueue.main.async {
222 | self.objectWillChange.send()
223 | }
224 | }
225 | }
226 |
227 | var relatedUser = [String: User]()
228 |
229 | // MARK: Helpers
230 | func storeRelatedUser(_ user: User?) {
231 | guard let userObjectId = user?.id,
232 | relatedUser[userObjectId] == nil else {
233 | return
234 | }
235 | relatedUser[userObjectId] = user
236 | }
237 |
238 | func isLikedPost(_ post: Post, userObjectId: String? = nil) -> Bool {
239 | let userOfInterest: String!
240 | if let user = userObjectId {
241 | userOfInterest = user
242 | } else {
243 | guard let currentUser = User.current?.id else {
244 | Logger.home.error("User is suppose to be logged")
245 | return false
246 | }
247 | userOfInterest = currentUser
248 | }
249 | guard let activities = likes[post.id],
250 | activities.first(where: { $0.fromUser?.objectId == userOfInterest }) != nil else {
251 | return false
252 | }
253 | return true
254 | }
255 |
256 | func isCommentedOnPost(_ post: Post, userObjectId: String? = nil) -> Bool {
257 | let userOfInterest: String!
258 | if let user = userObjectId {
259 | userOfInterest = user
260 | } else {
261 | guard let currentUser = User.current?.id else {
262 | Logger.home.error("User is suppose to be logged")
263 | return false
264 | }
265 | userOfInterest = currentUser
266 | }
267 | guard let activities = comments[post.id],
268 | activities.first(where: { $0.fromUser?.objectId == userOfInterest }) != nil else {
269 | return false
270 | }
271 | return true
272 | }
273 | }
274 |
275 | // MARK: ParseLiveQuery
276 | public extension ParseLiveQuery {
277 | internal func subscribeCustom(_ query: Query) throws -> QueryImageViewModel {
278 | try subscribe(QueryImageViewModel(query: query))
279 | }
280 | }
281 |
282 | // MARK: QueryImageViewModel
283 | public extension Query {
284 |
285 | /**
286 | Registers the query for live updates, using the default subscription handler,
287 | and the default `ParseLiveQuery` client. Suitable for `ObjectObserved`
288 | as the subscription can be used as a SwiftUI publisher. Meaning it can serve
289 | indepedently as a ViewModel in MVVM.
290 | */
291 | internal var subscribeCustom: QueryImageViewModel? {
292 | try? ParseLiveQuery.client?.subscribeCustom(self)
293 | }
294 |
295 | /**
296 | Registers the query for live updates, using the default subscription handler,
297 | and a specific `ParseLiveQuery` client. Suitable for `ObjectObserved`
298 | as the subscription can be used as a SwiftUI publisher. Meaning it can serve
299 | indepedently as a ViewModel in MVVM.
300 | - parameter client: A specific client.
301 | - returns: The subscription that has just been registered
302 | */
303 | internal func subscribeCustom(_ client: ParseLiveQuery) async throws -> QueryImageViewModel {
304 | guard try await Utility.isServerAvailable() else {
305 | let errorMessage = "Server health is not \"ok\""
306 | Logger.queryImageViewModel.error("\(errorMessage)")
307 | throw SnapCatError(message: errorMessage)
308 | }
309 | return try client.subscribe(QueryImageViewModel(query: self))
310 | }
311 |
312 | /**
313 | Creates a view model for this query. Suitable for `ObjectObserved`
314 | as the view model can be used as a SwiftUI publisher. Meaning it can serve
315 | indepedently as a ViewModel in MVVM.
316 | */
317 | internal var imageViewModel: QueryImageViewModel {
318 | QueryImageViewModel(query: self)
319 | }
320 |
321 | /**
322 | Creates a view model for this query. Suitable for `ObjectObserved`
323 | as the view model can be used as a SwiftUI publisher. Meaning it can serve
324 | indepedently as a ViewModel in MVVM.
325 | - parameter query: Any query.
326 | - returns: The view model for this query.
327 | */
328 | internal static func imageViewModel(_ query: Self) -> QueryImageViewModel {
329 | QueryImageViewModel(query: query)
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/SnapCat/Environment/TintColorKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TintColorKey.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 1/8/22.
6 | // Copyright © 2022 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct TintColorKey: EnvironmentKey {
13 |
14 | static var defaultValue: UIColor {
15 | UIColor { $0.userInterfaceStyle == .light ? #colorLiteral(red: 0, green: 0.2858072221, blue: 0.6897063851, alpha: 1) : #colorLiteral(red: 0.06253327429, green: 0.6597633362, blue: 0.8644603491, alpha: 1) }
16 | }
17 | }
18 |
19 | extension EnvironmentValues {
20 |
21 | var tintColor: UIColor {
22 | self[TintColorKey.self]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SnapCat/Explore/EmptyExploreView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyExploreView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import UIKit
11 |
12 | struct EmptyExploreView: UIViewControllerRepresentable {
13 |
14 | func makeUIViewController(context: Context) -> some UIViewController {
15 | let view = EmptyExploreViewController()
16 | let viewController = UINavigationController(rootViewController: view)
17 | viewController.navigationBar.isHidden = true
18 | return viewController
19 | }
20 |
21 | func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
22 |
23 | }
24 | }
25 |
26 | struct EmptyExploreView_Previews: PreviewProvider {
27 | static var previews: some View {
28 | EmptyExploreView()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SnapCat/Explore/EmptyExploreViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyExploreViewController.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import DZNEmptyDataSet
12 |
13 | class EmptyExploreViewController: EmptyDefaultViewController {
14 |
15 | override func viewDidLoad() {
16 | super.viewDidLoad()
17 | }
18 |
19 | override func viewDidAppear(_ animated: Bool) {
20 | super.viewDidAppear(animated)
21 | }
22 |
23 | override func title(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString {
24 | let text = "Explore"
25 |
26 | let attributes = [
27 | NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 18.0),
28 | NSAttributedString.Key.foregroundColor: UIColor.darkGray
29 | ]
30 |
31 | return NSAttributedString(string: text, attributes: attributes)
32 | }
33 |
34 | override func description(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString {
35 | let text = "Follow, meet, and engage with people"
36 |
37 | let paragragh = NSMutableParagraphStyle()
38 | paragragh.lineBreakMode = NSLineBreakMode.byWordWrapping
39 | paragragh.alignment = NSTextAlignment.center
40 |
41 | let attributes = [
42 | NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 14.0),
43 | NSAttributedString.Key.foregroundColor: UIColor.lightGray,
44 | NSAttributedString.Key.paragraphStyle: paragragh
45 | ]
46 |
47 | return NSAttributedString(string: text, attributes: attributes)
48 | }
49 |
50 | override func buttonTitle(forEmptyDataSet scrollView: UIScrollView,
51 | for state: UIControl.State) -> NSAttributedString {
52 | let attributes = [
53 | NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 17.0)
54 | ]
55 |
56 | return NSAttributedString(string: "", attributes: attributes)
57 | }
58 |
59 | override func image(forEmptyDataSet scrollView: UIScrollView) -> UIImage? {
60 |
61 | if var image = UIImage(systemName: "magnifyingglass.circle") {
62 |
63 | image = image.tint(UIColor.lightGray, blendMode: CGBlendMode.color)
64 | image = image.imageRotatedByDegrees(180, flip: false)
65 | return image
66 | }
67 | return UIImage()
68 |
69 | }
70 |
71 | override func emptyDataSet(_ scrollView: UIScrollView, didTap view: UIView) {
72 |
73 | }
74 |
75 | override func emptyDataSet(_ scrollView: UIScrollView, didTap button: UIButton) {
76 |
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/SnapCat/Explore/ExploreView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExploreView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ExploreView: View {
12 |
13 | @Environment(\.tintColor) private var tintColor
14 | @StateObject var viewModel = ExploreViewModel()
15 | @State var searchText: String = ""
16 |
17 | var body: some View {
18 | if !viewModel.users.isEmpty {
19 | VStack {
20 | SearchBarView(searchText: $searchText)
21 | ScrollView {
22 | ForEach(viewModel
23 | .users
24 | .filter({
25 | // swiftlint:disable:next line_length
26 | searchText == "" ? true : $0.username!.lowercased().contains(searchText.lowercased())
27 | }), id: \.id) { user in
28 |
29 | HStack {
30 | NavigationLink(destination: ProfileView(user: user, isShowingHeading: false), label: {
31 | if let image = viewModel.profileImages[user.id] {
32 | Image(uiImage: image)
33 | .resizable()
34 | .frame(width: 50, height: 50, alignment: .leading)
35 | .clipShape(Circle())
36 | .shadow(radius: 3)
37 | .overlay(Circle().stroke(Color(tintColor), lineWidth: 1))
38 | .padding()
39 | } else {
40 | Image(systemName: "person.circle")
41 | .resizable()
42 | .frame(width: 50, height: 50, alignment: .leading)
43 | .clipShape(Circle())
44 | .shadow(radius: 3)
45 | .overlay(Circle().stroke(Color(tintColor), lineWidth: 1))
46 | .padding()
47 | }
48 | VStack(alignment: .leading) {
49 | if let username = user.username {
50 | Text("@\(username)")
51 | .font(.headline)
52 | }
53 | HStack {
54 | if let name = user.name {
55 | Text(name)
56 | .font(.footnote)
57 | }
58 | if viewModel.isCurrentFollower(user) {
59 | Label("Follows You",
60 | systemImage: "checkmark.square.fill")
61 | }
62 | }
63 | }
64 | Spacer()
65 | })
66 | if viewModel.isCurrentFollowing(user) {
67 | Button(action: {
68 | viewModel.unfollowUser(user)
69 | }, label: {
70 | Text("Unfollow")
71 | .foregroundColor(.white)
72 | .padding()
73 | .background(Color(#colorLiteral(red: 0.06253327429, green: 0.6597633362, blue: 0.8644603491, alpha: 1)))
74 | .cornerRadius(15)
75 | })
76 | } else {
77 | Button(action: {
78 | viewModel.followUser(user)
79 | }, label: {
80 | Text("Follow")
81 | .foregroundColor(.white)
82 | .padding()
83 | .background(Color(#colorLiteral(red: 0.7843137255, green: 0.7843137255, blue: 0.7843137255, alpha: 1)))
84 | .cornerRadius(15)
85 | })
86 | }
87 | }
88 | Divider()
89 | }
90 | .navigationBarHidden(true)
91 | Spacer()
92 | }
93 | .padding()
94 | }.onAppear(perform: {
95 | viewModel.update()
96 | })
97 | } else {
98 | VStack {
99 | EmptyExploreView()
100 | .onAppear(perform: {
101 | viewModel.update()
102 | })
103 | Spacer()
104 | }
105 | }
106 | }
107 | }
108 |
109 | struct ExploreView_Previews: PreviewProvider {
110 | static var previews: some View {
111 | ExploreView()
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/SnapCat/Explore/ExploreViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExploreViewModel.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 | import ParseSwift
12 | import UIKit
13 |
14 | @dynamicMemberLookup
15 | class ExploreViewModel: ObservableObject {
16 | var isSettingForFirstTime = true
17 | var isShowingFollowers: Bool?
18 | var followersViewModel: QueryViewModel? {
19 | willSet {
20 | if isSettingForFirstTime {
21 | guard let followers = newValue else {
22 | return
23 | }
24 | users = followers.results.compactMap { $0.fromUser }
25 | }
26 | }
27 | }
28 | var followingsViewModel: QueryViewModel? {
29 | willSet {
30 | if isSettingForFirstTime {
31 | guard let followings = newValue else {
32 | return
33 | }
34 | users = followings.results.compactMap { $0.toUser }
35 | }
36 | }
37 | }
38 | var users = [User]() {
39 | willSet {
40 | newValue.forEach { object in
41 | // Fetch images
42 | if profileImages.count == Constants.numberOfImagesToDownload {
43 | return
44 | }
45 | guard profileImages[object.id] == nil else {
46 | return
47 | }
48 | Task {
49 | if let image = await Utility.fetchImage(object.profileImage) {
50 | DispatchQueue.main.async {
51 | self.profileImages[object.id] = image
52 | }
53 | }
54 | }
55 | }
56 | objectWillChange.send()
57 | }
58 | }
59 |
60 | @Published var currentUserFollowers = [User]()
61 | @Published var currentUserFollowings = [User]()
62 |
63 | /// Contains all fetched images.
64 | @Published var profileImages = [String: UIImage]()
65 |
66 | subscript(dynamicMember member: String) -> [User] {
67 | return users
68 | }
69 |
70 | init(isShowingFollowers: Bool? = nil,
71 | followersViewModel: QueryViewModel? = nil,
72 | followingsViewModel: QueryViewModel? = nil) {
73 | DispatchQueue.main.async {
74 | if let isShowingFollowers = isShowingFollowers {
75 | guard let followersViewModel = followersViewModel,
76 | let followingsViewModel = followingsViewModel else {
77 | return
78 | }
79 | self.followersViewModel = followersViewModel
80 | self.followingsViewModel = followingsViewModel
81 | self.isShowingFollowers = isShowingFollowers
82 | if isShowingFollowers {
83 | self.updateFollowers()
84 | } else {
85 | self.updateFollowings()
86 | }
87 | }
88 | DispatchQueue.main.async {
89 | self.update()
90 | }
91 | }
92 | }
93 |
94 | // MARK: Helpers
95 | @MainActor
96 | func update() {
97 | self.isSettingForFirstTime = true
98 | if let isShowingFollowers = self.isShowingFollowers {
99 | DispatchQueue.main.async {
100 | if isShowingFollowers {
101 | self.updateFollowers()
102 | } else {
103 | self.updateFollowings()
104 | }
105 | }
106 | self.isSettingForFirstTime = false
107 | } else {
108 | Task {
109 | await queryUsersNotFollowing()
110 | }
111 | }
112 | Task {
113 | do {
114 | let activities = try await ProfileViewModel.queryFollowers().find()
115 | self.currentUserFollowers = activities.compactMap { $0.fromUser }
116 | } catch {
117 | Logger.explore.error("Failed to query current followers: \(error.localizedDescription)")
118 | }
119 | }
120 | Task {
121 | do {
122 | let activities = try await ProfileViewModel.queryFollowings().find()
123 | self.currentUserFollowings = activities.compactMap { $0.toUser }
124 | } catch {
125 | Logger.explore.error("Failed to query current followings: \(error.localizedDescription)")
126 | }
127 | }
128 | }
129 |
130 | // MARK: Intents
131 | @MainActor
132 | func followUser(_ user: User) {
133 | do {
134 | let newActivity = try Activity(type: .follow, from: User.current, to: user)
135 | .setupForFollowing()
136 | self.users = self.users.filter({ $0.objectId != user.objectId })
137 | Task {
138 | do {
139 | _ = try await newActivity.save()
140 | } catch {
141 | Logger.explore.error("Couldn't save follow: \(error.localizedDescription)")
142 | }
143 | }
144 | } catch {
145 | Logger.explore.error("Can't create follow activity \(error.localizedDescription)")
146 | }
147 | }
148 |
149 | @MainActor
150 | func unfollowUser(_ toUser: User) {
151 | guard let currentUser = User.current,
152 | let activity = followingsViewModel?.results.first(where: { activity in
153 | guard let activityToUser = activity.toUser,
154 | let activityFromUser = activity.fromUser,
155 | let activityType = activity.type,
156 | activityToUser.hasSameObjectId(as: toUser),
157 | activityFromUser.hasSameObjectId(as: currentUser),
158 | activityType == Activity.ActionType.follow else {
159 | return false
160 | }
161 | return true
162 | }) else {
163 | return
164 | }
165 | self.followingsViewModel?.results.removeAll(where: { $0.hasSameObjectId(as: activity) })
166 | self.updateFollowings()
167 | Task {
168 | do {
169 | try await activity.delete()
170 | } catch {
171 | Logger.explore.error("Couldn't delete activity \(error.localizedDescription)")
172 | }
173 | }
174 | }
175 |
176 | // MARK: Helpers
177 | @MainActor
178 | func queryUsersNotFollowing() async {
179 | guard let currentUserObjectId = User.current?.objectId else {
180 | Logger.explore.error("Couldn't get own objectId")
181 | return
182 | }
183 | Task {
184 | do {
185 | let foundUsers = try await ProfileViewModel.queryFollowings().find()
186 | var objectIds = foundUsers.compactMap { $0.toUser?.id }
187 | objectIds.append(currentUserObjectId)
188 | let query = User.query(notContainedIn(key: ParseKey.objectId, array: objectIds))
189 | do {
190 | self.users = try await query.find()
191 | } catch {
192 | Logger.explore.error("Couldn't query users: \(error.localizedDescription)")
193 | }
194 | } catch {
195 | Logger.explore.error("Couldn't find followings: \(error.localizedDescription)")
196 | }
197 | }
198 | }
199 |
200 | func isCurrentFollower(_ user: User?) -> Bool {
201 | guard let user = user else { return false }
202 | return currentUserFollowers.first(where: { $0.hasSameObjectId(as: user) }) != nil
203 | }
204 |
205 | func isCurrentFollowing(_ user: User?) -> Bool {
206 | guard let user = user else { return false }
207 | return currentUserFollowings.first(where: { $0.hasSameObjectId(as: user) }) != nil
208 | }
209 |
210 | @MainActor
211 | func updateFollowers() {
212 | guard let followersViewModel = followersViewModel else {
213 | return
214 | }
215 | self.users = followersViewModel.results.compactMap { $0.fromUser }
216 | }
217 |
218 | @MainActor
219 | func updateFollowings() {
220 | guard let followingsViewModel = followingsViewModel else {
221 | return
222 | }
223 | self.users = followingsViewModel.results.compactMap { $0.toUser }
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/SnapCat/Explore/SearchBarView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchBarView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/10/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct SearchBarView: View {
12 | @Binding var searchText: String
13 | @State private var isEditing = false
14 |
15 | var body: some View {
16 | HStack {
17 | TextField("Search...", text: $searchText)
18 | .padding()
19 | .textFieldStyle(RoundedBorderTextFieldStyle())
20 | .overlay(
21 | HStack {
22 | Spacer()
23 | if isEditing {
24 | Button(action: {
25 | self.cancelEditing()
26 | }, label: {
27 | Image(systemName: "multiply.circle.fill")
28 | .foregroundColor(.gray)
29 | .padding()
30 | })
31 | }
32 | }
33 | )
34 | .background(Color(.systemGray6))
35 | .onChange(of: searchText, perform: { value in
36 | if value != "" {
37 | self.isEditing = true
38 | }
39 | })
40 | Spacer()
41 | if isEditing {
42 | Button(action: {
43 | self.cancelEditing()
44 | }, label: {
45 | Text("Cancel")
46 | .padding()
47 | })
48 | }
49 | }
50 | }
51 |
52 | func cancelEditing() {
53 | self.isEditing = false
54 | self.searchText = ""
55 | UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
56 | }
57 | }
58 |
59 | struct SearchBarView_Previews: PreviewProvider {
60 | static var previews: some View {
61 | SearchBarView(searchText: .constant(""))
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/SnapCat/Extentions/Date+extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date+extension.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // swiftlint:disable line_length
12 |
13 | // Source: Leo Dabus, http://stackoverflow.com/questions/27310883/swift-ios-doesrelativedateformatting-have-different-values-besides-today-and
14 | extension Date {
15 | func yearsFrom(_ date: Date) -> Int {
16 | return (Calendar.current as NSCalendar).components(.year,
17 | from: date,
18 | to: self,
19 | options: []).year ?? 0
20 | }
21 | func monthsFrom(_ date: Date) -> Int {
22 | return (Calendar.current as NSCalendar).components(.month, from: date,
23 | to: self,
24 | options: []).month ?? 0
25 | }
26 | func weeksFrom(_ date: Date) -> Int {
27 | return (Calendar.current as NSCalendar).components(.weekOfYear,
28 | from: date,
29 | to: self,
30 | options: []).weekOfYear ?? 0
31 | }
32 | func daysFrom(_ date: Date) -> Int {
33 | return (Calendar.current as NSCalendar).components(.day,
34 | from: date,
35 | to: self,
36 | options: []).day ?? 0
37 | }
38 | func hoursFrom(_ date: Date) -> Int {
39 | return (Calendar.current as NSCalendar).components(.hour,
40 | from: date,
41 | to: self,
42 | options: []).hour ?? 0
43 | }
44 | func minutesFrom(_ date: Date) -> Int {
45 | return (Calendar.current as NSCalendar).components(.minute,
46 | from: date,
47 | to: self,
48 | options: []).minute ?? 0
49 | }
50 | func secondsFrom(_ date: Date) -> Int {
51 | return (Calendar.current as NSCalendar).components(.second,
52 | from: date,
53 | to: self,
54 | options: []).second ?? 0
55 | }
56 | var relativeTime: String {
57 | let now = Date()
58 | if now.yearsFrom(self) > 0 {
59 | return now.yearsFrom(self).description + " year" + { return now.yearsFrom(self) > 1 ? "s" : "" }() + " ago"
60 | }
61 | if now.monthsFrom(self) > 0 {
62 | return now.monthsFrom(self).description + " month" + { return now.monthsFrom(self) > 1 ? "s" : "" }() + " ago"
63 | }
64 | if now.weeksFrom(self) > 0 {
65 | return now.weeksFrom(self).description + " week" + { return now.weeksFrom(self) > 1 ? "s" : "" }() + " ago"
66 | }
67 | if now.daysFrom(self) > 0 {
68 | if daysFrom(self) == 1 { return "Yesterday" }
69 | return now.daysFrom(self).description + " days ago"
70 | }
71 | if now.hoursFrom(self) > 0 {
72 | return "\(now.hoursFrom(self)) hour" + { return now.hoursFrom(self) > 1 ? "s" : "" }() + " ago"
73 | }
74 | if now.minutesFrom(self) > 0 {
75 | return "\(now.minutesFrom(self)) minute" + { return now.minutesFrom(self) > 1 ? "s" : "" }() + " ago"
76 | }
77 | if now.secondsFrom(self) > 0 {
78 | if now.secondsFrom(self) < 15 { return "Just now" }
79 | return "\(now.secondsFrom(self)) second" + { return now.secondsFrom(self) > 1 ? "s" : "" }() + " ago"
80 | }
81 | return ""
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/SnapCat/Extentions/Logger+extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Logger+extension.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 |
12 | extension Logger {
13 | private static var subsystem = Bundle.main.bundleIdentifier!
14 | static let category = "SnapCat"
15 | static let user = Logger(subsystem: subsystem, category: "\(category).user")
16 | static let installation = Logger(subsystem: subsystem, category: "\(category).installation")
17 | static let activity = Logger(subsystem: subsystem, category: "\(category).activity")
18 | static let post = Logger(subsystem: subsystem, category: "\(category).post")
19 | static let onboarding = Logger(subsystem: subsystem, category: "\(category).onboarding")
20 | static let main = Logger(subsystem: subsystem, category: "\(category).main")
21 | static let home = Logger(subsystem: subsystem, category: "\(category).home")
22 | static let explore = Logger(subsystem: subsystem, category: "\(category).explore")
23 | static let profile = Logger(subsystem: subsystem, category: "\(category).profile")
24 | static let notification = Logger(subsystem: subsystem, category: "\(category).notification")
25 | static let utility = Logger(subsystem: subsystem, category: "\(category).utility")
26 | static let settings = Logger(subsystem: subsystem, category: "\(category).settings")
27 | static let comment = Logger(subsystem: subsystem, category: "\(category).comment")
28 | static let queryImageViewModel = Logger(subsystem: subsystem, category: "\(category).queryImageViewModel")
29 | }
30 |
--------------------------------------------------------------------------------
/SnapCat/Extentions/ParseSwift+extention.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParseSwift+extention.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 |
12 | // swiftlint:disable line_length
13 | // swiftlint:disable function_body_length
14 |
15 | extension Utility {
16 | /** Can setup a connection to Parse Server based on a ParseCareKit.plist file.
17 |
18 | The key/values supported in the file are a dictionary named `ParseClientConfiguration`:
19 | - Server - (String) The server URL to connect to Parse Server.
20 | - ApplicationID - (String) The application id of your Parse application.
21 | - ClientKey - (String) The client key of your Parse application.
22 | - LiveQueryServer - (String) The live query server URL to connect to Parse Server.
23 | - UseTransactions - (Boolean) Use transactions inside the Client SDK.
24 | - parameter authentication: A callback block that will be used to receive/accept/decline network challenges.
25 | Defaults to `nil` in which the SDK will use the default OS authentication methods for challenges.
26 | It should have the following argument signature: `(challenge: URLAuthenticationChallenge,
27 | completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void`.
28 | See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411595-urlsession) for more for details.
29 | */
30 | static func setupServer(authentication: ((URLAuthenticationChallenge,
31 | (URLSession.AuthChallengeDisposition,
32 | URLCredential?) -> Void) -> Void)? = nil) {
33 | var propertyListFormat = PropertyListSerialization.PropertyListFormat.xml
34 | var plistConfiguration: [String: AnyObject]
35 | var clientKey: String?
36 | var liveQueryURL: URL?
37 | var useTransactions = false
38 | var cacheMemoryCapacity = 512_000
39 | var cacheDiskCapacity = 10_000_000
40 | var deleteKeychainIfNeeded = false
41 |
42 | guard let path = Bundle.main.path(forResource: "ParseSwift", ofType: "plist"),
43 | let xml = FileManager.default.contents(atPath: path) else {
44 | fatalError("Error in Utility.setupServer(). Can't find ParseSwift.plist in this project")
45 | }
46 | do {
47 | plistConfiguration =
48 | try PropertyListSerialization.propertyList(from: xml,
49 | options: .mutableContainersAndLeaves,
50 | // swiftlint:disable:next force_cast
51 | format: &propertyListFormat) as! [String: AnyObject]
52 | } catch {
53 | fatalError("Error in Utility.setupServer(). Couldn't serialize plist. \(error)")
54 | }
55 |
56 | guard let appID = plistConfiguration["ApplicationID"] as? String,
57 | let server = plistConfiguration["Server"] as? String,
58 | let serverURL = URL(string: server) else {
59 | fatalError("Error in Utility.setupServer()")
60 | }
61 |
62 | if let client = plistConfiguration["ClientKey"] as? String {
63 | clientKey = client
64 | }
65 |
66 | if let liveQuery = plistConfiguration["LiveQueryServer"] as? String {
67 | liveQueryURL = URL(string: liveQuery)
68 | }
69 |
70 | if let transactions = plistConfiguration["UseTransactions"] as? Bool {
71 | useTransactions = transactions
72 | }
73 |
74 | if let capacity = plistConfiguration["CacheMemoryCapacity"] as? Int {
75 | cacheMemoryCapacity = capacity
76 | }
77 |
78 | if let capacity = plistConfiguration["CacheDiskCapacity"] as? Int {
79 | cacheDiskCapacity = capacity
80 | }
81 |
82 | if let deleteKeychain = plistConfiguration["DeleteKeychainIfNeeded"] as? Bool {
83 | deleteKeychainIfNeeded = deleteKeychain
84 | }
85 |
86 | ParseSwift.initialize(applicationId: appID,
87 | clientKey: clientKey,
88 | serverURL: serverURL,
89 | liveQueryServerURL: liveQueryURL,
90 | usingTransactions: useTransactions,
91 | requestCachePolicy: .reloadIgnoringLocalCacheData,
92 | cacheMemoryCapacity: cacheMemoryCapacity,
93 | cacheDiskCapacity: cacheDiskCapacity,
94 | deletingKeychainIfNeeded: deleteKeychainIfNeeded,
95 | authentication: authentication)
96 | }
97 |
98 | /**
99 | Check server health.
100 | - returns: **true** if the server is available. **false** if the server reponds with not healthy.
101 | - throws: `ParseError`.
102 | */
103 | static func isServerAvailable() async throws -> Bool {
104 | let serverHealth = try await ParseHealth.check()
105 | guard serverHealth.contains("ok") else {
106 | return false
107 | }
108 | return true
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/SnapCat/Extentions/UIImage+extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage+extension.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | // Source: https://gist.github.com/alexruperez/90f44545b57c25b977c4
13 | extension UIImage {
14 | func tint(_ color: UIColor, blendMode: CGBlendMode) -> UIImage {
15 | let drawRect = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
16 | UIGraphicsBeginImageContextWithOptions(size, false, scale)
17 |
18 | if let context = UIGraphicsGetCurrentContext(), let mask = cgImage {
19 | context.clip(to: drawRect, mask: mask)
20 | }
21 | color.setFill()
22 | UIRectFill(drawRect)
23 | draw(in: drawRect, blendMode: blendMode, alpha: 1.0)
24 |
25 | if let tintedImage = UIGraphicsGetImageFromCurrentImageContext() {
26 | UIGraphicsEndImageContext()
27 |
28 | return tintedImage
29 | }
30 | return UIImage()
31 | }
32 |
33 | // Source: http://stackoverflow.com/questions/29137488/how-do-i-resize-the-uiimage-to-reduce-upload-image-size
34 | func resize(_ scale: CGFloat) -> UIImage {
35 | let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0),
36 | size: CGSize(width: size.width*scale,
37 | height: size.height*scale)))
38 | imageView.contentMode = UIView.ContentMode.scaleAspectFit
39 | imageView.image = self
40 | UIGraphicsBeginImageContext(imageView.bounds.size)
41 | if let UIGraphicsGetCurrentContext = UIGraphicsGetCurrentContext() {
42 | imageView.layer.render(in: UIGraphicsGetCurrentContext)
43 | }
44 |
45 | let result = UIGraphicsGetImageFromCurrentImageContext()
46 | UIGraphicsEndImageContext()
47 | if let result = result {
48 | return result
49 | }
50 |
51 | return UIImage()
52 | }
53 | func resizeToWidth(_ width: CGFloat) -> UIImage {
54 | let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0),
55 | size: CGSize(width: width,
56 | height: width)))
57 | // imageView.contentMode = UIViewContentMode.ScaleAspectFit
58 | imageView.image = self
59 | UIGraphicsBeginImageContext(imageView.bounds.size)
60 |
61 | if let UIGraphicsGetCurrentContext = UIGraphicsGetCurrentContext() {
62 | imageView.layer.render(in: UIGraphicsGetCurrentContext)
63 | }
64 | let result = UIGraphicsGetImageFromCurrentImageContext()
65 | UIGraphicsEndImageContext()
66 | if let result = result {
67 | return result
68 | }
69 | return UIImage()
70 | }
71 |
72 | func resizeToWidthHalfHeight(_ width: CGFloat) -> UIImage {
73 | let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0),
74 | size: CGSize(width: width,
75 | height: CGFloat(ceil(width/size.width * size.height)))))
76 | // imageView.contentMode = UIViewContentMode.ScaleAspectFit
77 | imageView.image = self
78 | UIGraphicsBeginImageContext(imageView.bounds.size)
79 | if let UIGraphicsGetCurrentContext = UIGraphicsGetCurrentContext() {
80 | imageView.layer.render(in: UIGraphicsGetCurrentContext)
81 | }
82 |
83 | let result = UIGraphicsGetImageFromCurrentImageContext()
84 | UIGraphicsEndImageContext()
85 | if let result = result {
86 | return result
87 | }
88 | return UIImage()
89 | }
90 |
91 | // Source: https://ruigomes.me/blog/how-to-rotate-an-uiimage-using-swift/
92 | func imageRotatedByDegrees(_ degrees: CGFloat, flip: Bool) -> UIImage {
93 | /*let radiansToDegrees: (CGFloat) -> CGFloat = {
94 | return $0 * (180.0 / CGFloat(Double.pi))
95 | }*/
96 | let degreesToRadians: (CGFloat) -> CGFloat = {
97 | return $0 / 180.0 * CGFloat(Double.pi)
98 | }
99 |
100 | // calculate the size of the rotated view's containing box for our drawing space
101 | let rotatedViewBox = UIView(frame: CGRect(origin: CGPoint.zero, size: size))
102 | let transform = CGAffineTransform(rotationAngle: degreesToRadians(degrees))
103 | rotatedViewBox.transform = transform
104 | let rotatedSize = rotatedViewBox.frame.size
105 |
106 | // Create the bitmap context
107 | UIGraphicsBeginImageContext(rotatedSize)
108 | let bitmap = UIGraphicsGetCurrentContext()
109 |
110 | // Move the origin to the middle of the image so we will rotate and scale around the center.
111 | bitmap?.translateBy(x: rotatedSize.width / 2.0, y: rotatedSize.height / 2.0)
112 |
113 | // // Rotate the image context
114 | bitmap?.rotate(by: degreesToRadians(degrees))
115 |
116 | // Now, draw the rotated/scaled image into the context
117 | var yFlip: CGFloat
118 |
119 | if flip {
120 | yFlip = CGFloat(-1.0)
121 | } else {
122 | yFlip = CGFloat(1.0)
123 | }
124 |
125 | bitmap?.scaleBy(x: yFlip, y: -1.0)
126 | bitmap?.draw(cgImage!,
127 | in: CGRect(x: -size.width / 2,
128 | y: -size.height / 2,
129 | width: size.width,
130 | height: size.height))
131 |
132 | let newImage = UIGraphicsGetImageFromCurrentImageContext()
133 | UIGraphicsEndImageContext()
134 |
135 | return newImage!
136 | }
137 | }
138 |
139 | // swiftlint:disable:next line_length
140 | // Source: https://stackoverflow.com/questions/29726643/how-to-compress-of-reduce-the-size-of-an-image-before-uploading-to-parse-as-pffi/29726675
141 | extension UIImage {
142 | // MARK: - UIImage+Resize
143 | func compressTo(_ expectedSizeInMb: Int) -> Data? {
144 | let sizeInBytes = expectedSizeInMb * 1024 * 1024
145 | var needCompress: Bool = true
146 | var imgData: Data?
147 | var compressingValue: CGFloat = 1.0
148 | while needCompress && compressingValue > 0.0 {
149 | if let data: Data = self.jpegData(compressionQuality: compressingValue) {
150 | if data.count < sizeInBytes {
151 | needCompress = false
152 | imgData = data
153 | } else {
154 | compressingValue -= 0.1
155 | }
156 | }
157 | }
158 |
159 | if let data = imgData {
160 | if data.count < sizeInBytes {
161 | return data
162 | }
163 | }
164 |
165 | return nil
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/SnapCat/Extentions/View+extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+extension.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | extension View {
12 | func formattedHostingController() -> UIHostingController {
13 | let viewController = UIHostingController(rootView: self)
14 | viewController.view.backgroundColor = UIColor { $0.userInterfaceStyle == .light ? .white : .black }
15 | return viewController
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/SnapCat/Home/CommentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommentView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/16/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ParseSwift
11 | import os.log
12 |
13 | struct CommentView: View {
14 | @ObservedObject var viewModel: CommentViewModel
15 | @ObservedObject var timeLineViewModel: QueryImageViewModel
16 | @Environment(\.presentationMode) var presentationMode
17 |
18 | var body: some View {
19 | VStack {
20 | Form {
21 | Section {
22 | TextField("Add a comment", text: $viewModel.comment)
23 | }
24 | }
25 | .navigationBarBackButtonHidden(true)
26 | .navigationTitle(Text("Comment"))
27 | .navigationBarItems(leading: Button(action: {
28 | self.presentationMode.wrappedValue.dismiss()
29 | }, label: {
30 | Text("Cancel")
31 | }), trailing: Button(action: {
32 | Task {
33 | do {
34 | let comment = try await viewModel.save()
35 | if let postId = viewModel.activity?.post?.id {
36 | timeLineViewModel.comments[postId]?.insert(comment, at: 0)
37 | }
38 | } catch {
39 | Logger.comment.error("Error saving: \(error.localizedDescription)")
40 | }
41 | }
42 | self.presentationMode.wrappedValue.dismiss()
43 | }, label: {
44 | Text("Done")
45 | }))
46 | }
47 | }
48 |
49 | init(timeLineViewModel: QueryImageViewModel,
50 | post: Post,
51 | activity: Activity? = nil) {
52 | self.timeLineViewModel = timeLineViewModel
53 | viewModel = CommentViewModel(post: post, activity: activity)
54 | }
55 | }
56 |
57 | struct CommentView_Previews: PreviewProvider {
58 | static var previews: some View {
59 | CommentView(timeLineViewModel: .init(query: Post.query()), post: Post())
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/SnapCat/Home/CommentViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommentViewModel.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/16/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 | import ParseSwift
12 | import UIKit
13 |
14 | @MainActor
15 | class CommentViewModel: ObservableObject {
16 | @Published var activity: Activity?
17 | @Published var comment = ""
18 |
19 | init(post: Post? = nil, activity: Activity? = nil) {
20 | if activity != nil {
21 | self.activity = activity
22 | } else {
23 | self.activity = Activity(type: .comment, from: User.current, to: post?.user)
24 | self.activity?.post = post
25 | }
26 | }
27 |
28 | // MARK: Intents
29 | @MainActor
30 | func save() async throws -> Activity {
31 | guard var currentActivity = activity else {
32 | return Activity()
33 | }
34 | if !comment.isEmpty {
35 | currentActivity.comment = comment
36 | activity = currentActivity
37 | return try await currentActivity.save()
38 | } else {
39 | return currentActivity
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/SnapCat/Home/EmptyTimeLineView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyTimeLineView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import UIKit
11 |
12 | struct EmptyTimeLineView: UIViewControllerRepresentable {
13 |
14 | func makeUIViewController(context: Context) -> some UIViewController {
15 | let view = EmptyTimeLineViewController()
16 | let viewController = UINavigationController(rootViewController: view)
17 | viewController.navigationBar.isHidden = true
18 | return viewController
19 | }
20 |
21 | func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
22 |
23 | }
24 | }
25 |
26 | struct EmptyTimeLineView_Previews: PreviewProvider {
27 | static var previews: some View {
28 | EmptyTimeLineView()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SnapCat/Home/EmptyTimeLineViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyTimeLineViewController.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import DZNEmptyDataSet
12 |
13 | class EmptyTimeLineViewController: EmptyDefaultViewController {
14 |
15 | override func viewDidLoad() {
16 | super.viewDidLoad()
17 | }
18 |
19 | override func viewDidAppear(_ animated: Bool) {
20 | super.viewDidAppear(animated)
21 | }
22 |
23 | override func title(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString {
24 | let text = "Timeline"
25 |
26 | let attributes = [
27 | NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 18.0),
28 | NSAttributedString.Key.foregroundColor: UIColor.darkGray
29 | ]
30 |
31 | return NSAttributedString(string: text, attributes: attributes)
32 | }
33 |
34 | override func description(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString {
35 | let text = "See posts from people you follow"
36 |
37 | let paragragh = NSMutableParagraphStyle()
38 | paragragh.lineBreakMode = NSLineBreakMode.byWordWrapping
39 | paragragh.alignment = NSTextAlignment.center
40 |
41 | let attributes = [
42 | NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 14.0),
43 | NSAttributedString.Key.foregroundColor: UIColor.lightGray,
44 | NSAttributedString.Key.paragraphStyle: paragragh
45 | ]
46 |
47 | return NSAttributedString(string: text, attributes: attributes)
48 | }
49 |
50 | override func buttonTitle(forEmptyDataSet scrollView: UIScrollView,
51 | for state: UIControl.State) -> NSAttributedString {
52 | let attributes = [
53 | NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 17.0)
54 | ]
55 |
56 | return NSAttributedString(string: "Tap here to find friends", attributes: attributes)
57 | }
58 |
59 | override func image(forEmptyDataSet scrollView: UIScrollView) -> UIImage? {
60 |
61 | if var image = UIImage(systemName: "house") {
62 |
63 | image = image.tint(UIColor.lightGray, blendMode: CGBlendMode.color)
64 | image = image.imageRotatedByDegrees(180, flip: false)
65 | return image
66 | }
67 | return UIImage()
68 |
69 | }
70 |
71 | override func emptyDataSet(_ scrollView: UIScrollView, didTap view: UIView) {
72 | presentView()
73 | }
74 |
75 | override func emptyDataSet(_ scrollView: UIScrollView, didTap button: UIButton) {
76 | presentView()
77 | }
78 |
79 | func presentView() {
80 | let friendsViewController = ExploreView().formattedHostingController()
81 | friendsViewController.modalPresentationStyle = .popover
82 | present(friendsViewController, animated: true, completion: nil)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/SnapCat/Home/HomeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ParseSwift
11 |
12 | struct HomeView: View {
13 |
14 | @Environment(\.tintColor) private var tintColor
15 | @ObservedObject var timeLineViewModel: QueryImageViewModel
16 | @StateObject var postStatus = PostStatus()
17 |
18 | var body: some View {
19 | VStack {
20 | NavigationLink(destination: PostView(timeLineViewModel: timeLineViewModel)
21 | .environmentObject(postStatus),
22 | isActive: $postStatus.isShowing) {
23 | EmptyView()
24 | }
25 | HStack {
26 | Text("SnapCat")
27 | .font(Font.custom("noteworthy-bold", size: 30))
28 | .foregroundColor(Color(tintColor))
29 | .padding()
30 | Spacer()
31 | Button(action: {
32 | self.postStatus.isShowing = true
33 | }, label: {
34 | Image(systemName: "square.and.pencil")
35 | .resizable()
36 | .foregroundColor(Color(tintColor))
37 | .frame(width: 30, height: 30, alignment: .trailing)
38 | .padding()
39 | })
40 | }
41 | Divider()
42 | TimeLineView(viewModel: timeLineViewModel)
43 | }
44 | }
45 |
46 | init() {
47 | let timeLineQuery = TimeLineViewModel.queryTimeLine()
48 | .include(PostKey.user)
49 | if let timeLine = timeLineQuery.subscribeCustom {
50 | timeLineViewModel = timeLine
51 | } else {
52 | timeLineViewModel = timeLineQuery.imageViewModel
53 | }
54 | timeLineViewModel.find()
55 | }
56 | }
57 |
58 | struct HomeView_Previews: PreviewProvider {
59 | static var previews: some View {
60 | HomeView()
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/SnapCat/Home/HomeViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewModel.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 | import ParseSwift
12 |
13 | class HomeViewModel: ObservableObject {
14 |
15 | // MARK: Queries
16 | }
17 |
--------------------------------------------------------------------------------
/SnapCat/Home/ImagePickerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImagePickerView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/11/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | // swiftlint:disable line_length
10 |
11 | // Source: https://www.hackingwithswift.com/books/ios-swiftui/importing-an-image-into-swiftui-using-uiimagepickercontroller
12 |
13 | import SwiftUI
14 | import UIKit
15 |
16 | struct ImagePickerView: UIViewControllerRepresentable {
17 | @Environment(\.presentationMode) var presentationMode
18 | @Binding var image: UIImage?
19 |
20 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController {
21 | let picker = UIImagePickerController()
22 | picker.delegate = context.coordinator
23 | return picker
24 | }
25 |
26 | func updateUIViewController(_ uiViewController: UIImagePickerController,
27 | context: UIViewControllerRepresentableContext) {
28 |
29 | }
30 |
31 | func makeCoordinator() -> Coordinator {
32 | Coordinator(self)
33 | }
34 |
35 | class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
36 | let parent: ImagePickerView
37 |
38 | init(_ parent: ImagePickerView) {
39 | self.parent = parent
40 | }
41 |
42 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
43 | DispatchQueue.main.async {
44 | if let uiImage = info[.originalImage] as? UIImage {
45 | self.parent.image = uiImage
46 | }
47 | self.parent.presentationMode.wrappedValue.dismiss()
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/SnapCat/Home/PostView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/11/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ParseSwift
11 |
12 | struct PostView: View {
13 |
14 | @Environment(\.presentationMode) var presentationMode
15 | @EnvironmentObject var postStatus: PostStatus
16 | @ObservedObject var timeLineViewModel: QueryImageViewModel
17 | @StateObject var viewModel = PostViewModel()
18 | @State private var isShowingImagePicker = false
19 |
20 | var body: some View {
21 | VStack {
22 | GeometryReader { geometry in
23 | Form {
24 | Section {
25 | HStack {
26 | Spacer()
27 | Button(action: {
28 | self.isShowingImagePicker = true
29 | }, label: {
30 | if let image = viewModel.image {
31 | Image(uiImage: image)
32 | .resizable()
33 | .frame(width: 0.75 * geometry.size.width,
34 | height: 0.75 * geometry.size.width,
35 | alignment: .center)
36 | .clipShape(Rectangle())
37 | .scaledToFill()
38 | } else {
39 | Image(systemName: "camera")
40 | .resizable()
41 | .frame(width: 200, height: 200, alignment: .center)
42 | .clipShape(Rectangle())
43 | .padding()
44 | }
45 | })
46 | .buttonStyle(PlainButtonStyle())
47 | Spacer()
48 | }
49 |
50 | TextField("Caption", text: $viewModel.caption)
51 | if let placeMark = viewModel.currentPlacemark,
52 | let name = placeMark.name {
53 | Text(name)
54 | } else {
55 | Text("Location: N/A")
56 | }
57 | }
58 | Section {
59 | Button(action: {
60 | viewModel.requestPermission()
61 | }, label: {
62 | if viewModel.currentPlacemark == nil {
63 | Text("Use Location")
64 | } else {
65 | Text("Remove Location")
66 | }
67 | })
68 | }
69 | }
70 | .navigationBarBackButtonHidden(true)
71 | .navigationTitle(Text("Post"))
72 | .navigationBarItems(leading: Button(action: {
73 | self.postStatus.isShowing = false
74 | }, label: {
75 | Text("Cancel")
76 | }), trailing: Button(action: {
77 | Task {
78 | _ = try await viewModel.save()
79 | }
80 | self.postStatus.isShowing = false
81 | }, label: {
82 | Text("Done")
83 | }))
84 | .sheet(isPresented: $isShowingImagePicker, onDismiss: {}, content: {
85 | ImagePickerView(image: $viewModel.image)
86 | })
87 | }
88 | }
89 | }
90 | }
91 |
92 | struct PostView_Previews: PreviewProvider {
93 | static var previews: some View {
94 | PostView(timeLineViewModel: .init(query: Post.query()))
95 | .environmentObject(PostStatus(isShowing: true))
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/SnapCat/Home/PostViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostViewModel.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/11/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 | import ParseSwift
12 | import UIKit
13 | import CoreLocation
14 |
15 | class PostViewModel: NSObject, ObservableObject {
16 | @Published var post: Post?
17 | @Published var image: UIImage?
18 | @Published var caption = ""
19 | @Published var location: ParseGeoPoint?
20 | var currentPlacemark: CLPlacemark? {
21 | willSet {
22 | if let currentLocation = newValue?.location {
23 | location = try? ParseGeoPoint(location: currentLocation)
24 | } else {
25 | location = nil
26 | }
27 | objectWillChange.send()
28 | }
29 | }
30 | private var authorizationStatus: CLAuthorizationStatus
31 | private var lastSeenLocation: CLLocation?
32 | private let locationManager: CLLocationManager
33 |
34 | init(post: Post? = nil) {
35 | if post != nil {
36 | self.post = post
37 | } else {
38 | self.post = Post(image: nil)
39 | }
40 | locationManager = CLLocationManager()
41 | authorizationStatus = locationManager.authorizationStatus
42 |
43 | super.init()
44 | locationManager.delegate = self
45 | locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
46 | locationManager.startUpdatingLocation()
47 | }
48 |
49 | // MARK: Intents
50 | func requestPermission() {
51 | locationManager.requestWhenInUseAuthorization()
52 | }
53 |
54 | func save() async throws -> Post {
55 | guard let image = image,
56 | let compressed = image.compressTo(3),
57 | var currentPost = post else {
58 | return Post()
59 | }
60 | currentPost.image = ParseFile(data: compressed)
61 | currentPost.caption = caption
62 | currentPost.location = location
63 | return try await currentPost.save()
64 | }
65 |
66 | // MARK: Queries
67 | class func queryLikes(post: Post?) -> Query {
68 | guard let pointer = try? post?.toPointer() else {
69 | Logger.home.error("Should have created pointer.")
70 | return Activity.query().limit(0)
71 | }
72 | let query = Activity.query(ActivityKey.post == pointer,
73 | ActivityKey.type == Activity.ActionType.like)
74 | .order([.descending(ParseKey.createdAt)])
75 | return query
76 | }
77 |
78 | class func queryComments(post: Post?) -> Query {
79 | guard let pointer = try? post?.toPointer() else {
80 | Logger.home.error("Should have created pointer.")
81 | return Activity.query().limit(0)
82 | }
83 | let query = Activity.query(ActivityKey.post == pointer,
84 | ActivityKey.type == Activity.ActionType.comment)
85 | .order([.descending(ParseKey.createdAt)])
86 | return query
87 | }
88 | }
89 |
90 | // MARK: CLLocationManagerDelegate
91 |
92 | // Source: https://www.andyibanez.com/posts/using-corelocation-with-swiftui/
93 | extension PostViewModel: CLLocationManagerDelegate {
94 | func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
95 | authorizationStatus = manager.authorizationStatus
96 | }
97 |
98 | func locationManager(_ manager: CLLocationManager,
99 | didUpdateLocations locations: [CLLocation]) {
100 | lastSeenLocation = locations.first
101 | fetchCountryAndCity(for: locations.first)
102 | }
103 |
104 | func fetchCountryAndCity(for location: CLLocation?) {
105 | guard let location = location else { return }
106 | let geocoder = CLGeocoder()
107 | geocoder.reverseGeocodeLocation(location) { (placemarks, _) in
108 | self.currentPlacemark = placemarks?.first
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/SnapCat/Home/TimeLineCommentsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimeLineCommentsView.swift
3 | // TimeLineCommentsView
4 | //
5 | // Created by Corey Baker on 7/17/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct TimeLineCommentsView: View {
12 | @ObservedObject var timeLineViewModel: QueryImageViewModel
13 | @State var post: Post
14 | @State var isShowingAllComments = false
15 | @State var postSelected = Post()
16 | var body: some View {
17 | VStack {
18 | if let comments = timeLineViewModel.comments[post.id],
19 | comments.count > 0 {
20 | NavigationLink(destination: ViewAllComments(timeLineViewModel: timeLineViewModel,
21 | post: post),
22 | isActive: $isShowingAllComments) {
23 | EmptyView()
24 | }
25 | if comments.count > 1 {
26 | HStack {
27 | Text("View all \(comments.count) comments")
28 | .font(.footnote)
29 | .onTapGesture(count: 1) {
30 | self.postSelected = post
31 | self.isShowingAllComments = true
32 | }
33 | Spacer()
34 | }
35 | }
36 | HStack {
37 | if let username = comments.first?.fromUser?.username {
38 | Text("\(username)")
39 | .font(.headline)
40 | }
41 | if let lastComment = comments.first?.comment {
42 | Text(lastComment)
43 | }
44 | Spacer()
45 | }
46 | }
47 | if let createdAt = post.createdAt {
48 | HStack {
49 | Text(createdAt.relativeTime)
50 | .font(.footnote)
51 | Spacer()
52 | }
53 | }
54 | }
55 | }
56 | }
57 |
58 | struct TimeLineCommentsView_Previews: PreviewProvider {
59 | static var previews: some View {
60 | TimeLineCommentsView(timeLineViewModel: .init(query: Post.query()), post: Post())
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/SnapCat/Home/TimeLineLikeCommentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimeLineLikeCommentView.swift
3 | // TimeLineLikeCommentView
4 | //
5 | // Created by Corey Baker on 7/17/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct TimeLineLikeCommentView: View {
12 | @ObservedObject var timeLineViewModel: QueryImageViewModel
13 | @State var post: Post
14 | @State var isShowingComment = false
15 | @State var postSelected: Post?
16 | let currentObjectId: String
17 | var body: some View {
18 | VStack {
19 | HStack {
20 | VStack {
21 | if timeLineViewModel.isLikedPost(post,
22 | userObjectId: currentObjectId) {
23 | Image(systemName: "heart.fill")
24 | } else {
25 | Image(systemName: "heart")
26 | }
27 | }.onTapGesture(count: 1) {
28 | let currentTimeLine = timeLineViewModel
29 | Task {
30 | let (activity, status) = await TimeLineViewModel.likePost(post,
31 | currentLikes: currentTimeLine
32 | .likes[post.id])
33 | switch status {
34 |
35 | case .like:
36 | timeLineViewModel.likes[post.id]?.append(activity)
37 | case .unlike:
38 | timeLineViewModel.likes[post.id]?.removeAll(where: {$0.hasSameObjectId(as: activity)})
39 | case .error:
40 | break
41 | }
42 |
43 | }
44 | }
45 | VStack {
46 | if timeLineViewModel.isCommentedOnPost(post,
47 | userObjectId: currentObjectId) {
48 | Image(systemName: "bubble.left.fill")
49 | } else {
50 | Image(systemName: "bubble.left")
51 | }
52 | }.onTapGesture(count: 1) {
53 | self.postSelected = post
54 | self.isShowingComment = true
55 | }
56 | Spacer()
57 | }
58 | HStack {
59 | if let likes = timeLineViewModel.likes[post.id] {
60 | Text("Liked by")
61 | if likes.count > 2 {
62 | if let lastLikeUsername = likes.last?.fromUser?.username {
63 | Text("\(lastLikeUsername) ")
64 | .font(.headline)
65 | }
66 | Text("and \(likes.count - 1) others")
67 | } else if likes.count == 1 {
68 | Text("\(likes.count) person")
69 | } else {
70 | Text("\(likes.count) people")
71 | }
72 | Spacer()
73 | }
74 | }
75 | HStack {
76 | if let username = post.user?.username {
77 | Text("\(username)")
78 | .font(.headline)
79 | }
80 | if let caption = post.caption {
81 | Text(caption)
82 | }
83 | Spacer()
84 | }
85 | }.sheet(isPresented: $isShowingComment, content: {
86 | if let post = self.postSelected {
87 | CommentView(timeLineViewModel: timeLineViewModel,
88 | post: post)
89 | }
90 | })
91 | }
92 | }
93 |
94 | struct TimeLineLikeCommentView_Previews: PreviewProvider {
95 | static var previews: some View {
96 | TimeLineLikeCommentView(timeLineViewModel: .init(query: Post.query()), post: Post(), currentObjectId: "")
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/SnapCat/Home/TimeLinePostView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimeLinePostView.swift
3 | // TimeLinePostView
4 | //
5 | // Created by Corey Baker on 7/17/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | // swiftlint:disable line_length
12 |
13 | struct TimeLinePostView: View {
14 | @ObservedObject var timeLineViewModel: QueryImageViewModel
15 | @State var post: Post
16 | var body: some View {
17 | VStack {
18 | GeometryReader { geometry in
19 | HStack {
20 | if let image = timeLineViewModel.imageResults[post.id] {
21 | Spacer()
22 | Image(uiImage: image)
23 | .resizable()
24 | .frame(width: 0.95 * geometry.size.width,
25 | height: 0.95 * geometry.size.width,
26 | alignment: .leading)
27 | .clipShape(Rectangle())
28 | .onTapGesture(count: 2) {
29 | Task {
30 | let currentTimeLine = timeLineViewModel
31 |
32 | let (activity, status) = await TimeLineViewModel.likePost(post,
33 | currentLikes: currentTimeLine
34 | .likes[post.id])
35 | switch status {
36 |
37 | case .like:
38 | timeLineViewModel.likes[post.id]?.append(activity)
39 | case .unlike:
40 | timeLineViewModel
41 | .likes[post.id]?
42 | .removeAll(where: { $0.hasSameObjectId(as: activity) })
43 | case .error:
44 | break
45 | }
46 | }
47 | }
48 | Spacer()
49 | } else {
50 | Image(systemName: "camera")
51 | .resizable()
52 | .clipShape(Rectangle())
53 | }
54 | }
55 | }
56 | }
57 | }
58 | }
59 |
60 | struct TimeLineImageView_Previews: PreviewProvider {
61 | static var previews: some View {
62 | TimeLinePostView(timeLineViewModel: .init(query: Post.query()),
63 | post: Post())
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/SnapCat/Home/TimeLineView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimeLineView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/5/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ParseSwift
11 | import os.log
12 | import UIKit
13 |
14 | struct TimeLineView: View {
15 |
16 | @Environment(\.tintColor) private var tintColor
17 | @ObservedObject var timeLineViewModel: QueryImageViewModel
18 | @State var isShowingProfile = false
19 | @State var gradient = LinearGradient(gradient: Gradient(colors: [Color(#colorLiteral(red: 0, green: 0.2858072221, blue: 0.6897063851, alpha: 1)), Color(#colorLiteral(red: 0.06253327429, green: 0.6597633362, blue: 0.8644603491, alpha: 1))]),
20 | startPoint: .top,
21 | endPoint: .bottom)
22 | @State var userTapped = User()
23 | let currentObjectId: String
24 | var body: some View {
25 | if !timeLineViewModel.results.isEmpty {
26 | NavigationLink(destination: ProfileView(user: userTapped,
27 | isShowingHeading: false)
28 | .navigationTitle("")
29 | .navigationBarHidden(false),
30 | isActive: $isShowingProfile) {
31 | EmptyView()
32 | }
33 | NavigationView {
34 | List(timeLineViewModel.results, id: \.id) { result in
35 | VStack {
36 | HStack {
37 | if let userObjectId = result.user?.id,
38 | let image = timeLineViewModel.imageResults[userObjectId] {
39 | Image(uiImage: image)
40 | .resizable()
41 | .frame(width: 40, height: 40, alignment: .leading)
42 | .clipShape(Circle())
43 | .shadow(radius: 3)
44 | .overlay(Circle().stroke(gradient, lineWidth: 3))
45 | } else {
46 | Image(systemName: "person.circle")
47 | .resizable()
48 | .frame(width: 40, height: 40, alignment: .leading)
49 | .clipShape(Circle())
50 | .shadow(radius: 3)
51 | .overlay(Circle().stroke(gradient, lineWidth: 1))
52 | }
53 | if let username = result.user?.username {
54 | Text("\(username)")
55 | .font(.headline)
56 | }
57 | Spacer()
58 | }.onTapGesture(count: 1) {
59 | if let user = result.user {
60 | self.timeLineViewModel.userOfInterest = user
61 | self.userTapped = user
62 | self.isShowingProfile = true
63 | }
64 | }
65 | TimeLinePostView(timeLineViewModel: timeLineViewModel,
66 | post: result)
67 | .scaledToFill()
68 | TimeLineLikeCommentView(timeLineViewModel: timeLineViewModel,
69 | post: result,
70 | currentObjectId: currentObjectId)
71 | TimeLineCommentsView(timeLineViewModel: timeLineViewModel, post: result)
72 |
73 | Spacer()
74 | }
75 | }.navigationBarHidden(true)
76 | }.onAppear(perform: {
77 | timeLineViewModel.find()
78 | })
79 | } else {
80 | VStack {
81 | EmptyTimeLineView()
82 | Spacer()
83 | }
84 | }
85 | }
86 |
87 | init(viewModel: QueryImageViewModel? = nil) {
88 | if let objectId = User.current?.id {
89 | currentObjectId = objectId
90 | } else {
91 | currentObjectId = ""
92 | }
93 | guard let viewModel = viewModel else {
94 | let timeLineQuery = TimeLineViewModel.queryTimeLine()
95 | .include(PostKey.user)
96 | if let timeLine = timeLineQuery.subscribeCustom {
97 | timeLineViewModel = timeLine
98 | } else {
99 | timeLineViewModel = timeLineQuery.imageViewModel
100 | }
101 | timeLineViewModel.find()
102 | return
103 | }
104 | timeLineViewModel = viewModel
105 | }
106 | }
107 |
108 | struct TimeLineView_Previews: PreviewProvider {
109 | static var previews: some View {
110 | TimeLineView()
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/SnapCat/Home/TimeLineViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimeLineViewModel.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/5/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 | import ParseSwift
12 |
13 | @MainActor
14 | class TimeLineViewModel: ObservableObject {
15 |
16 | // MARK: Intents
17 | class func likePost(_ post: Post,
18 | currentLikes: [Activity]?) async -> (Activity, Activity.LikeState) {
19 | guard let alreadyLikes = currentLikes?
20 | .first(where: { User.current?.id == $0.fromUser?.id }) else {
21 | let likeActivity = Activity.like(post: post)
22 | do {
23 | let liked = try await likeActivity.save()
24 | return (liked, .like)
25 | } catch {
26 | Logger.home.error("Error liking post \(post): Error: \(error.localizedDescription)")
27 | return (likeActivity, .error)
28 | }
29 | }
30 | do {
31 | try await alreadyLikes.delete()
32 | return (alreadyLikes, .unlike)
33 | } catch {
34 | Logger.home.error("Error deleting like: \(error.localizedDescription)")
35 | return (alreadyLikes, .error)
36 | }
37 | }
38 |
39 | // MARK: Queries
40 | class func queryTimeLine() -> Query {
41 | guard let pointer = try? User.current?.toPointer() else {
42 | Logger.home.error("Should have created pointer.")
43 | return Post.query().limit(0)
44 | }
45 |
46 | let findFollowings = ProfileViewModel.queryFollowings()
47 | let findTimeLineData = Post.query(matchesKeyInQuery(key: PostKey.user,
48 | queryKey: ActivityKey.toUser,
49 | query: findFollowings))
50 | let findTimeLineDataForCurrentUser = Post.query(PostKey.user == pointer)
51 | let subQueries = [findTimeLineData, findTimeLineDataForCurrentUser]
52 | let query = Post.query(or(queries: subQueries))
53 | .order([.descending(ParseKey.createdAt)])
54 | return query
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/SnapCat/Home/ViewAllComments.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewAllComments.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/17/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ViewAllComments: View {
12 | @Environment(\.presentationMode) var presentationMode
13 | @ObservedObject var timeLineViewModel: QueryImageViewModel
14 | @State var post: Post
15 | var body: some View {
16 | if let comments = timeLineViewModel.comments[post.id] {
17 | List(comments, id: \.id) { result in
18 | VStack(alignment: .leading) {
19 | HStack {
20 | if let username = result.fromUser?.username {
21 | Text("\(username)")
22 | .font(.headline)
23 | }
24 | if let lastComment = result.comment {
25 | Text(lastComment)
26 | }
27 | }
28 | if let createdAt = result.createdAt {
29 | HStack {
30 | Text(createdAt.relativeTime)
31 | .font(.footnote)
32 | Spacer()
33 | }
34 | }
35 | }
36 | }
37 | .navigationBarBackButtonHidden(true)
38 | .navigationTitle(Text("Comments"))
39 | .navigationBarItems(leading: Button(action: {
40 | self.presentationMode.wrappedValue.dismiss()
41 | }, label: {
42 | Text("Cancel")
43 | }), trailing: Button(action: {
44 | self.presentationMode.wrappedValue.dismiss()
45 | }, label: {
46 | Text("Done")
47 | }))
48 | } else {
49 | EmptyView()
50 | }
51 | }
52 | }
53 |
54 | struct ViewAllComments_Previews: PreviewProvider {
55 | static var previews: some View {
56 | ViewAllComments(timeLineViewModel: .init(query: Post.query()),
57 | post: Post())
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/SnapCat/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 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 |
28 | UIApplicationSupportsIndirectInputEvents
29 |
30 | UILaunchScreen
31 |
32 | UIImageRespectsSafeAreaInsets
33 |
34 | UIImageName
35 | Snapcat
36 |
37 | UIRequiredDeviceCapabilities
38 |
39 | armv7
40 |
41 | UISupportedInterfaceOrientations
42 |
43 | UIInterfaceOrientationPortrait
44 | UIInterfaceOrientationLandscapeLeft
45 | UIInterfaceOrientationLandscapeRight
46 |
47 | UISupportedInterfaceOrientations~ipad
48 |
49 | UIInterfaceOrientationPortrait
50 | UIInterfaceOrientationPortraitUpsideDown
51 | UIInterfaceOrientationLandscapeLeft
52 | UIInterfaceOrientationLandscapeRight
53 |
54 | NSAppTransportSecurity
55 |
56 | NSAllowsArbitraryLoads
57 |
58 | NSExceptionDomains
59 |
60 | localhost
61 |
62 | NSAllowsArbitraryLoads
63 |
64 |
65 |
66 |
67 | NSLocationWhenInUseUsageDescription
68 | Will only use your location whenever you post a message when the app is open.
69 | NSPhotoLibraryUsageDescription
70 | Post photos from your library
71 |
72 |
73 |
--------------------------------------------------------------------------------
/SnapCat/Main/EmptyDefaultViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyDefaultViewController.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import DZNEmptyDataSet
12 |
13 | class EmptyDefaultViewController: UITableViewController {
14 |
15 | let defaultImage = UIImage(systemName: "house")
16 |
17 | deinit {
18 | self.tableView?.emptyDataSetSource = nil
19 | self.tableView?.emptyDataSetDelegate = nil
20 | }
21 |
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 | self.tableView?.emptyDataSetSource = self
25 | self.tableView?.emptyDataSetDelegate = self
26 | self.tableView?.tableFooterView = UIView()
27 | }
28 | }
29 |
30 | // UITableViewDataSource, UITableViewDelegate
31 | extension EmptyDefaultViewController {
32 | override func numberOfSections(in tableView: UITableView) -> Int {
33 | return 1
34 | }
35 |
36 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
37 | return 0
38 | }
39 |
40 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
41 | return UITableViewCell()
42 | }
43 | }
44 |
45 | // DZNEmptyDataSet Delegates. More info about how to use here: https://github.com/dzenbot/DZNEmptyDataSet
46 | extension EmptyDefaultViewController: DZNEmptyDataSetSource {
47 | func title(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString? {
48 | let text = "Empty Data"
49 |
50 | let attributes = [
51 | NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 18.0),
52 | NSAttributedString.Key.foregroundColor: UIColor.darkGray
53 | ]
54 |
55 | return NSAttributedString(string: text, attributes: attributes)
56 | }
57 |
58 | func description(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString? {
59 | let text = "Empty data"
60 |
61 | let paragragh = NSMutableParagraphStyle()
62 | paragragh.lineBreakMode = NSLineBreakMode.byWordWrapping
63 | paragragh.alignment = NSTextAlignment.center
64 |
65 | let attributes = [
66 | NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 14.0),
67 | NSAttributedString.Key.foregroundColor: UIColor.lightGray,
68 | NSAttributedString.Key.paragraphStyle: paragragh
69 | ]
70 |
71 | return NSAttributedString(string: text, attributes: attributes)
72 | }
73 |
74 | func buttonTitle(forEmptyDataSet scrollView: UIScrollView, for state: UIControl.State) -> NSAttributedString? {
75 | let attributes = [
76 | NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 17.0)
77 | ]
78 |
79 | return NSAttributedString(string: "Tap here", attributes: attributes)
80 | }
81 |
82 | func image(forEmptyDataSet scrollView: UIScrollView) -> UIImage? {
83 | if var image = self.defaultImage {
84 | image = image.tint(UIColor.lightGray, blendMode: CGBlendMode.color)
85 | image = image.imageRotatedByDegrees(180, flip: false)
86 | return image
87 | }
88 | return UIImage()
89 | }
90 |
91 | func backgroundColor(forEmptyDataSet scrollView: UIScrollView) -> UIColor? {
92 | return UIColor.white
93 | }
94 | }
95 |
96 | extension EmptyDefaultViewController: DZNEmptyDataSetDelegate {
97 | func emptyDataSetShouldDisplay(_ scrollView: UIScrollView) -> Bool {
98 | return true
99 | }
100 |
101 | func emptyDataSetShouldAllowTouch(_ scrollView: UIScrollView) -> Bool {
102 | return true
103 | }
104 |
105 | func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView) -> Bool {
106 | return false
107 | }
108 |
109 | func emptyDataSet(_ scrollView: UIScrollView, didTap view: UIView) {
110 |
111 | }
112 |
113 | func emptyDataSet(_ scrollView: UIScrollView, didTap button: UIButton) {
114 |
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/SnapCat/Main/MainView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct MainView: View {
12 |
13 | @Environment(\.tintColor) private var tintColor
14 | @StateObject var userStatus = UserStatus()
15 | @State private var selectedTab = 0
16 |
17 | var body: some View {
18 |
19 | NavigationView {
20 | VStack {
21 | NavigationLink(destination: OnboardingView(),
22 | isActive: $userStatus.isLoggedOut) {
23 | EmptyView()
24 | }
25 |
26 | TabView(selection: $selectedTab) {
27 |
28 | HomeView()
29 | .tabItem {
30 | if selectedTab == 0 {
31 | Image(systemName: "house.fill")
32 | .renderingMode(.template)
33 | } else {
34 | Image(systemName: "house")
35 | .renderingMode(.template)
36 | }
37 | }
38 | .tag(0)
39 | .navigationBarTitle("")
40 | .navigationBarHidden(true)
41 |
42 | ExploreView()
43 | .tabItem {
44 | if selectedTab == 1 {
45 | Image(systemName: "magnifyingglass.circle.fill")
46 | .renderingMode(.template)
47 | } else {
48 | Image(systemName: "magnifyingglass.circle")
49 | .renderingMode(.template)
50 | }
51 | }
52 | .tag(1)
53 | .navigationBarTitle("")
54 | .navigationBarHidden(true)
55 |
56 | ActivityView()
57 | .tabItem {
58 | if selectedTab == 2 {
59 | Image(systemName: "heart.fill")
60 | .renderingMode(.template)
61 | } else {
62 | Image(systemName: "heart")
63 | .renderingMode(.template)
64 | }
65 | }
66 | .tag(2)
67 | .navigationBarTitle("")
68 | .navigationBarHidden(true)
69 |
70 | ProfileView()
71 | .tabItem {
72 | if selectedTab == 3 {
73 | Image(systemName: "person.fill")
74 | .renderingMode(.template)
75 | } else {
76 | Image(systemName: "person")
77 | .renderingMode(.template)
78 | }
79 | }
80 | .tag(3)
81 | .navigationBarTitle("")
82 | .navigationBarHidden(true)
83 | }
84 | }
85 | }
86 | .environmentObject(userStatus)
87 | .accentColor(Color(tintColor))
88 | .statusBar(hidden: true)
89 | .navigationViewStyle(StackNavigationViewStyle())
90 | }
91 | }
92 |
93 | struct MainView_Previews: PreviewProvider {
94 | static var previews: some View {
95 | MainView()
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/SnapCat/Models/Activity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Activity.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/3/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 |
12 | struct Activity: ParseObject {
13 |
14 | var objectId: String?
15 | var createdAt: Date?
16 | var updatedAt: Date?
17 | var ACL: ParseACL?
18 | var originalData: Data?
19 |
20 | var fromUser: User?
21 | var toUser: User?
22 | var type: ActionType?
23 | var comment: String?
24 | var post: Post?
25 | var activity: Pointer?
26 |
27 | enum ActionType: String, Codable {
28 | case like
29 | case follow
30 | case comment
31 | }
32 |
33 | enum LikeState: String, Codable {
34 | case like
35 | case unlike
36 | case error
37 | }
38 |
39 | func merge(with object: Self) throws -> Self {
40 | var updated = try mergeParse(with: object)
41 | if updated.shouldRestoreKey(\.fromUser,
42 | original: object) {
43 | updated.fromUser = object.fromUser
44 | }
45 | if updated.shouldRestoreKey(\.toUser,
46 | original: object) {
47 | updated.toUser = object.toUser
48 | }
49 | if updated.shouldRestoreKey(\.type,
50 | original: object) {
51 | updated.type = object.type
52 | }
53 | if updated.shouldRestoreKey(\.comment,
54 | original: object) {
55 | updated.comment = object.comment
56 | }
57 | if updated.shouldRestoreKey(\.post,
58 | original: object) {
59 | updated.post = object.post
60 | }
61 | if updated.shouldRestoreKey(\.activity,
62 | original: object) {
63 | updated.activity = object.activity
64 | }
65 | return updated
66 | }
67 |
68 | func setupForFollowing() throws -> Activity {
69 | var activity = self
70 | if activity.type == .follow {
71 | guard let followUser = activity.toUser else {
72 | throw SnapCatError(message: "missing \(ActivityKey.toUser)")
73 | }
74 | activity.ACL?.setWriteAccess(user: followUser, value: true)
75 | activity.ACL?.setReadAccess(user: followUser, value: true)
76 | } else {
77 | throw SnapCatError(message: "Can't setup for following for type: \"\(String(describing: type))\"")
78 | }
79 | return activity
80 | }
81 |
82 | static func like(post: Post) -> Self {
83 | var activity = Activity(type: .like, from: User.current, to: post.user)
84 | activity.post = post
85 | return activity
86 | }
87 | }
88 |
89 | extension Activity {
90 |
91 | init(type: ActionType, from fromUser: User?, to toUser: User?) {
92 | self.type = type
93 | self.fromUser = fromUser
94 | self.toUser = toUser
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/SnapCat/Models/Installation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Installation.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/3/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 |
12 | struct Installation: ParseInstallation {
13 | var deviceType: String?
14 |
15 | var installationId: String?
16 |
17 | var deviceToken: String?
18 |
19 | var badge: Int?
20 |
21 | var timeZone: String?
22 |
23 | var channels: [String]?
24 |
25 | var appName: String?
26 |
27 | var appIdentifier: String?
28 |
29 | var appVersion: String?
30 |
31 | var parseVersion: String?
32 |
33 | var localeIdentifier: String?
34 |
35 | var objectId: String?
36 |
37 | var createdAt: Date?
38 |
39 | var updatedAt: Date?
40 |
41 | var ACL: ParseACL?
42 |
43 | var originalData: Data?
44 |
45 | var user: User?
46 |
47 | func merge(with object: Self) throws -> Self {
48 | var updated = try mergeParse(with: object)
49 | if updated.shouldRestoreKey(\.user,
50 | original: object) {
51 | updated.user = object.user
52 | }
53 | return updated
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/SnapCat/Models/Post.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Post.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/3/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 |
12 | struct Post: ParseObject {
13 |
14 | var objectId: String?
15 | var createdAt: Date?
16 | var updatedAt: Date?
17 | var ACL: ParseACL?
18 | var originalData: Data?
19 |
20 | var user: User?
21 | var image: ParseFile?
22 | var thumbnail: ParseFile?
23 | var location: ParseGeoPoint?
24 | var caption: String?
25 |
26 | func merge(with object: Self) throws -> Self {
27 | var updated = try mergeParse(with: object)
28 | if updated.shouldRestoreKey(\.user,
29 | original: object) {
30 | updated.user = object.user
31 | }
32 | if updated.shouldRestoreKey(\.image,
33 | original: object) {
34 | updated.image = object.image
35 | }
36 | if updated.shouldRestoreKey(\.thumbnail,
37 | original: object) {
38 | updated.thumbnail = object.thumbnail
39 | }
40 | if updated.shouldRestoreKey(\.location,
41 | original: object) {
42 | updated.location = object.location
43 | }
44 | if updated.shouldRestoreKey(\.caption,
45 | original: object) {
46 | updated.caption = object.caption
47 | }
48 | return updated
49 | }
50 | }
51 |
52 | extension Post {
53 | init(image: ParseFile?) {
54 | user = User.current
55 | self.image = image
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/SnapCat/Models/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // User.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/3/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 |
12 | struct User: ParseUser {
13 |
14 | // Mandatory properties
15 | var authData: [String: [String: String]?]?
16 | var username: String?
17 | var email: String?
18 | var emailVerified: Bool?
19 | var password: String?
20 | var objectId: String?
21 | var createdAt: Date?
22 | var updatedAt: Date?
23 | var ACL: ParseACL?
24 | var originalData: Data?
25 |
26 | // Custom properties
27 | var name: String?
28 | var profileImage: ParseFile?
29 | var profileThumbnail: ParseFile?
30 | var bio: String?
31 | var link: URL?
32 |
33 | func merge(with object: Self) throws -> Self {
34 | var updated = try mergeParse(with: object)
35 | if updated.shouldRestoreKey(\.name,
36 | original: object) {
37 | updated.name = object.name
38 | }
39 | if updated.shouldRestoreKey(\.profileImage,
40 | original: object) {
41 | updated.profileImage = object.profileImage
42 | }
43 | if updated.shouldRestoreKey(\.profileThumbnail,
44 | original: object) {
45 | updated.profileThumbnail = object.profileThumbnail
46 | }
47 | if updated.shouldRestoreKey(\.bio,
48 | original: object) {
49 | updated.bio = object.bio
50 | }
51 | if updated.shouldRestoreKey(\.link,
52 | original: object) {
53 | updated.link = object.link
54 | }
55 | return updated
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/SnapCat/Onboarding/OnboardingView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/3/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ParseSwift
11 | import AuthenticationServices
12 |
13 | struct OnboardingView: View {
14 | @Environment(\.tintColor) private var tintColor
15 | @EnvironmentObject var userStatus: UserStatus
16 | @StateObject private var viewModel = OnboardingViewModel()
17 | @State var gradient = LinearGradient(gradient: Gradient(colors: [Color(#colorLiteral(red: 0.7843137255, green: 0.7843137255, blue: 0.7843137255, alpha: 1)), Color(#colorLiteral(red: 0, green: 0.2858072221, blue: 0.6897063851, alpha: 1))]),
18 | startPoint: .top,
19 | endPoint: .bottom)
20 | @State private var usersname = ""
21 | @State private var password = ""
22 | @State var name: String = ""
23 | @State private var signupLoginSegmentValue = 0
24 | @State private var presentMainScreen = false
25 |
26 | var body: some View {
27 |
28 | VStack {
29 |
30 | Text("SnapCat")
31 | .font(Font.custom("noteworthy-bold", size: 40))
32 | .foregroundColor(.white)
33 | .padding([.top], 40)
34 |
35 | Image("Snapcat")
36 | .resizable()
37 | .frame(width: 250, height: 250, alignment: .center)
38 | .clipShape(Circle())
39 | .overlay(Circle().stroke(gradient, lineWidth: 4))
40 | .shadow(radius: 10)
41 | .padding()
42 |
43 | Picker(selection: $signupLoginSegmentValue, label: Text("Login Picker"), content: {
44 | Text("Login").tag(0)
45 | Text("Sign Up").tag(1)
46 | })
47 | .pickerStyle(SegmentedPickerStyle())
48 | .background(Color.white)
49 | .cornerRadius(20.0)
50 | .padding()
51 |
52 | VStack(alignment: .leading) {
53 | TextField("Username", text: $usersname)
54 | .padding()
55 | .background(Color.white)
56 | .cornerRadius(20.0)
57 | .shadow(radius: 10.0, x: 20, y: 10)
58 |
59 | SecureField("Password", text: $password)
60 | .padding()
61 | .background(Color.white)
62 | .cornerRadius(20.0)
63 | .shadow(radius: 10.0, x: 20, y: 10)
64 |
65 | if signupLoginSegmentValue == 1 {
66 | TextField("Name", text: $name)
67 | .padding()
68 | .background(Color.white)
69 | .cornerRadius(20.0)
70 | .shadow(radius: 10.0, x: 20, y: 10)
71 | }
72 | }.padding([.leading, .trailing], 27.5)
73 |
74 | Button(action: {
75 | if signupLoginSegmentValue == 1 {
76 | Task {
77 | await viewModel.signup(username: usersname,
78 | password: password,
79 | name: name)
80 | }
81 | } else {
82 | Task {
83 | await viewModel.login(username: usersname, password: password)
84 | }
85 | }
86 |
87 | }, label: {
88 |
89 | if signupLoginSegmentValue == 1 {
90 | Text("Sign Up")
91 | .font(.headline)
92 | .foregroundColor(.white)
93 | .padding()
94 | .frame(width: 300, height: 50)
95 | } else {
96 | Text("Login")
97 | .font(.headline)
98 | .foregroundColor(.white)
99 | .padding()
100 | .frame(width: 300, height: 50)
101 | }
102 | })
103 | .background(Color(.green))
104 | .cornerRadius(15)
105 |
106 | Button(action: {
107 | Task {
108 | await viewModel.loginAnonymously()
109 | }
110 |
111 | }, label: {
112 |
113 | if signupLoginSegmentValue == 1 {
114 | Text("Login Anonymously")
115 | .font(.headline)
116 | .foregroundColor(.white)
117 | .padding()
118 | .frame(width: 300, height: 50)
119 | } else {
120 | Text("Login Anonymously")
121 | .font(.headline)
122 | .foregroundColor(.white)
123 | .padding()
124 | .frame(width: 300, height: 50)
125 | }
126 | })
127 | .background(Color(#colorLiteral(red: 0.06253327429, green: 0.6597633362, blue: 0.8644603491, alpha: 1)))
128 | .cornerRadius(15)
129 |
130 | SignInWithAppleButton(.signIn,
131 | onRequest: { (request) in
132 | request.requestedScopes = [.fullName, .email]
133 | },
134 | onCompletion: { (result) in
135 |
136 | switch result {
137 | case .success(let authorization):
138 | Task {
139 | await viewModel.loginWithApple(authorization: authorization)
140 | }
141 | case .failure(let error):
142 | viewModel.loginError = SnapCatError(message: error.localizedDescription)
143 | }
144 | })
145 | .frame(width: 300, height: 50)
146 | .cornerRadius(15)
147 |
148 | // If error occurs show it on the screen
149 | if let error = viewModel.loginError {
150 | Text("Error: \(error.message)")
151 | .foregroundColor(.red)
152 | }
153 |
154 | Spacer()
155 | }
156 | .onReceive(viewModel.$isLoggedOut, perform: { value in
157 | if self.userStatus.isLoggedOut != value {
158 | self.userStatus.check()
159 | }
160 | })
161 | .background(LinearGradient(gradient: Gradient(colors: [Color(#colorLiteral(red: 0, green: 0.2858072221, blue: 0.6897063851, alpha: 1)), Color(#colorLiteral(red: 0.7843137255, green: 0.7843137255, blue: 0.7843137255, alpha: 1))]),
162 | startPoint: .top,
163 | endPoint: .bottom))
164 | .edgesIgnoringSafeArea(.all)
165 | .signInWithAppleButtonStyle(.black)
166 | .navigationBarHidden(true)
167 | .navigationBarBackButtonHidden(true)
168 | }
169 | }
170 |
171 | struct OnboardingView_Previews: PreviewProvider {
172 | static var previews: some View {
173 | OnboardingView()
174 | .environmentObject(UserStatus(isLoggedOut: false))
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/SnapCat/Onboarding/OnboardingViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingViewModel.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/3/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 | import AuthenticationServices
12 | import os.log
13 |
14 | // swiftlint:disable cyclomatic_complexity
15 |
16 | class OnboardingViewModel: ObservableObject {
17 |
18 | @Published private(set) var isLoggedOut = true
19 | @Published var loginError: SnapCatError?
20 |
21 | init() {
22 | if User.current != nil {
23 | DispatchQueue.main.async {
24 | self.isLoggedOut = false
25 | }
26 | }
27 | }
28 |
29 | // MARK: Intents
30 | /**
31 | Signs up the user *asynchronously*.
32 |
33 | This will also enforce that the username isn't already taken.
34 |
35 | - parameter username: The username the user is signing in with.
36 | - parameter password: The password the user is signing in with.
37 | - parameter name: The name the user is signing in with.
38 | */
39 | @MainActor
40 | func signup(username: String, password: String, name: String) async {
41 | var user = User()
42 | user.username = username
43 | user.password = password
44 | user.name = name
45 | do {
46 | guard try await Utility.isServerAvailable() else {
47 | Logger.onboarding.error("Server health is not \"ok\"")
48 | return
49 | }
50 | let user = try await user.signup()
51 | Logger.onboarding.debug("Signup Successful: \(user)")
52 | self.completeOnboarding()
53 | } catch {
54 | guard let parseError = error as? ParseError else {
55 | Logger.onboarding.error("\(error.localizedDescription)")
56 | return
57 | }
58 | Logger.onboarding.error("\(parseError)")
59 | switch parseError.code {
60 | case .usernameTaken: // Account already exists for this username.
61 | self.loginError = SnapCatError(parseError: parseError)
62 |
63 | default:
64 | // There was a different issue that we don't know how to handle
65 | Logger.onboarding.error("""
66 | *** Error Signing up as user for Parse Server. Are you running parse-hipaa
67 | and is the initialization complete? Check http://localhost:1337 in your
68 | browser. If you are still having problems check for help here:
69 | https://github.com/netreconlab/parse-postgres#getting-started ***
70 | """)
71 | self.loginError = SnapCatError(parseError: parseError)
72 | }
73 | }
74 | }
75 |
76 | /**
77 | Logs in the user *asynchronously*.
78 |
79 | This will also enforce that the username isn't already taken.
80 |
81 | - parameter username: The username the user is logging in with.
82 | - parameter password: The password the user is logging in with.
83 | */
84 | @MainActor
85 | func login(username: String, password: String) async {
86 | do {
87 | guard try await Utility.isServerAvailable() else {
88 | Logger.onboarding.error("Server health is not \"ok\"")
89 | return
90 | }
91 | let user = try await User.login(username: username, password: password)
92 | Logger.onboarding.debug("Login Success: \(user, privacy: .private)")
93 | self.completeOnboarding()
94 | } catch {
95 | guard let parseError = error as? ParseError else {
96 | Logger.onboarding.error("\(error.localizedDescription)")
97 | return
98 | }
99 | Logger.onboarding.error("""
100 | *** Error logging into Parse Server. If you are still having problems
101 | check for help here:
102 | https://github.com/netreconlab/parse-hipaa#getting-started ***
103 | """)
104 | Logger.onboarding.debug("Login Error: \(parseError)")
105 | self.loginError = SnapCatError(parseError: parseError)
106 | }
107 |
108 | }
109 |
110 | /**
111 | Logs in the user anonymously *asynchronously*.
112 | */
113 | @MainActor
114 | func loginAnonymously() async {
115 | do {
116 | guard try await Utility.isServerAvailable() else {
117 | Logger.onboarding.error("Server health is not \"ok\"")
118 | return
119 | }
120 | let user = try await User.anonymous.login()
121 | Logger.onboarding.debug("Anonymous Login Success: \(user, privacy: .private)")
122 | self.completeOnboarding()
123 | } catch {
124 | guard let parseError = error as? ParseError else {
125 | Logger.onboarding.error("\(error.localizedDescription)")
126 | return
127 | }
128 | Logger.onboarding.error("""
129 | *** Error logging into Parse Server. If you are still having
130 | problems check for help here:
131 | https://github.com/netreconlab/parse-hipaa#getting-started ***
132 | """)
133 | Logger.onboarding.error("Anonymous Login Error: \(parseError)")
134 | self.loginError = SnapCatError(parseError: parseError)
135 | }
136 | }
137 |
138 | /**
139 | Logs in the user with Apple *asynchronously*.
140 | - parameter authorization: The encapsulation of a successful authorization performed by a controller..
141 | */
142 | @MainActor
143 | func loginWithApple(authorization: ASAuthorization) async { // swiftlint:disable:this function_body_length
144 | guard let credentials = authorization.credential as? ASAuthorizationAppleIDCredential,
145 | let identityToken = credentials.identityToken else {
146 | let error = "Failed unwrapping Apple authorization credentials."
147 | Logger.onboarding.error("Apple Login Error: \(error)")
148 | self.loginError = SnapCatError(message: error)
149 | return
150 | }
151 | do {
152 | guard try await Utility.isServerAvailable() else {
153 | Logger.onboarding.error("Server health is not \"ok\"")
154 | return
155 | }
156 | var user = try await User.apple.login(user: credentials.user, identityToken: identityToken)
157 | var isUpdatedUser = false
158 | if credentials.email != nil {
159 | user.email = credentials.email
160 | isUpdatedUser = true
161 | }
162 | if user.name == nil {
163 | if let name = credentials.fullName {
164 | var currentName = ""
165 | if let givenName = name.givenName {
166 | currentName = givenName
167 | }
168 | if let familyName = name.familyName {
169 | if currentName != "" {
170 | currentName = "\(currentName) \(familyName)"
171 | } else {
172 | currentName = familyName
173 | }
174 | }
175 | user.name = currentName
176 | isUpdatedUser = true
177 | }
178 | }
179 | let loggedInUser: User!
180 | if isUpdatedUser {
181 | loggedInUser = try await user.save()
182 | } else {
183 | loggedInUser = user
184 | }
185 | Logger.onboarding.debug("Apple Login Success: \(loggedInUser, privacy: .private)")
186 | self.completeOnboarding()
187 | } catch {
188 | guard let parseError = error as? ParseError else {
189 | Logger.onboarding.error("\(error.localizedDescription)")
190 | return
191 | }
192 | Logger.onboarding.error("Apple Login Error: \(parseError)")
193 | self.loginError = SnapCatError(parseError: parseError)
194 | }
195 | }
196 |
197 | // MARK: - Helper Methods
198 | @MainActor
199 | func completeOnboarding() {
200 | Self.setDefaultACL()
201 | saveInstallation()
202 | registerForNotifications()
203 | isLoggedOut = false
204 | }
205 |
206 | func saveInstallation() {
207 | // Setup installation to receive push notifications
208 | guard var currentInstallation = Installation.current else {
209 | return
210 | }
211 | currentInstallation.user = User.current
212 | currentInstallation.channels = ["global"] // Subscribe to particular channels
213 | let installation = currentInstallation
214 | Task {
215 | do {
216 | let installation = try await installation.save()
217 | Logger.installation.debug("""
218 | Parse Installation saved, can now receive
219 | push notificaitons. \(installation, privacy: .private)
220 | """)
221 | } catch {
222 | Logger.installation.debug("Error saving Parse Installation saved: \(error.localizedDescription)")
223 | }
224 | }
225 | }
226 |
227 | func registerForNotifications() {
228 | UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (allowed, error) in
229 | if allowed {
230 | Logger.notification.debug("User allowed notifications")
231 | } else {
232 | Logger.notification.debug("\(error.debugDescription)")
233 | }
234 | }
235 | }
236 |
237 | class func setDefaultACL() {
238 | if User.current != nil {
239 | // Set default ACL for all ParseObjects
240 | var defaultACL = ParseACL()
241 | defaultACL.publicRead = true
242 | defaultACL.publicWrite = false
243 | do {
244 | _ = try ParseACL.setDefaultACL(defaultACL, withAccessForCurrentUser: true)
245 | } catch {
246 | guard let parseError = error as? ParseError else {
247 | Logger.main.error("Error setting default ACL: \(error.localizedDescription)")
248 | return
249 | }
250 | Logger.main.error("Error setting default ACL: \(parseError)")
251 | }
252 | }
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/SnapCat/ParseSwift.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ApplicationID
6 | E036A0C5-6829-4B40-9B3B-3E05F6DF32B2
7 | Server
8 | http://localhost:1337/parse
9 | UseTransactions
10 |
11 | CacheMemoryCapacity
12 | 60000000
13 | CacheDiskCapacity
14 | 100000000
15 | DeleteKeychainIfNeeded
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/SnapCat/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SnapCat/Profile/ProfileEditView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileEditView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/10/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import os.log
11 | import ParseSwift
12 |
13 | struct ProfileEditView: View {
14 | @ObservedObject var viewModel: ProfileViewModel
15 | @Environment(\.presentationMode) var presentationMode
16 | @State var isShowAlert = false
17 | @State var isPasswordResetSuccess = false
18 |
19 | var body: some View {
20 | NavigationView {
21 | Form {
22 | Section {
23 | TextField("Username", text: $viewModel.username)
24 | TextField("Email", text: $viewModel.email)
25 | TextField("Name", text: $viewModel.name)
26 | TextField("Bio", text: $viewModel.bio)
27 | TextField("Link", text: $viewModel.link)
28 | }
29 |
30 | Section {
31 | Button(action: {
32 | Task {
33 | do {
34 | try await viewModel.resetPassword()
35 | self.isPasswordResetSuccess = true
36 | } catch {
37 | Logger.profile.error("\(error.localizedDescription)")
38 | }
39 | self.isShowAlert = true
40 | }
41 | }, label: {
42 | Text("Reset Password")
43 | })
44 | }
45 | }
46 | .navigationBarBackButtonHidden(true)
47 | .navigationTitle(Text("Edit Profile"))
48 | .navigationBarItems(leading: Button(action: {
49 | self.presentationMode.wrappedValue.dismiss()
50 | }, label: {
51 | Text("Cancel")
52 | }), trailing: Button(action: {
53 | Task {
54 | do {
55 | _ = try await viewModel.saveUpdates()
56 | self.presentationMode.wrappedValue.dismiss()
57 | } catch {
58 | guard let parseError = error as? SnapCatError else {
59 | return
60 | }
61 | if parseError.message.contains("No new changes") {
62 | self.presentationMode.wrappedValue.dismiss()
63 | return
64 | }
65 | self.isShowAlert = true
66 | }
67 | }
68 | }, label: {
69 | Text("Done")
70 | }))
71 | .alert(isPresented: $isShowAlert, content: {
72 | if let error = viewModel.error {
73 | return Alert(title: Text("Error"),
74 | message: Text(error.message),
75 | dismissButton: .default(Text("Ok"), action: {
76 | self.viewModel.error = nil
77 | })
78 | )
79 | } else if self.isPasswordResetSuccess {
80 | return Alert(title: Text("Password Reset"),
81 | message: Text("Please check your email for directions on how to reset your password"),
82 | dismissButton: .default(Text("Ok"), action: {
83 | self.presentationMode.wrappedValue.dismiss()
84 | })
85 | )
86 | } else {
87 | return Alert(title: Text("Updates Saved"),
88 | message: Text("All changes saved!"),
89 | dismissButton: .default(Text("Ok"), action: {
90 | self.presentationMode.wrappedValue.dismiss()
91 | })
92 | )
93 | }
94 |
95 | })
96 | }
97 | }
98 | }
99 |
100 | struct ProfileEditView_Previews: PreviewProvider {
101 | static var previews: some View {
102 | ProfileEditView(viewModel: ProfileViewModel(user: nil,
103 | isShowingHeading: true))
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/SnapCat/Profile/ProfileHeaderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileHeaderView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/18/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ProfileHeaderView: View {
12 | @Environment(\.tintColor) private var tintColor
13 | @StateObject var postStatus = PostStatus()
14 | @ObservedObject var viewModel: ProfileViewModel
15 | @ObservedObject var timeLineViewModel: QueryImageViewModel
16 | @State var isShowingOptions = false
17 | var body: some View {
18 | HStack {
19 | NavigationLink(destination: PostView(timeLineViewModel: timeLineViewModel)
20 | .environmentObject(postStatus),
21 | isActive: $postStatus.isShowing) {
22 | EmptyView()
23 | }
24 | NavigationLink(destination: SettingsView(),
25 | isActive: $isShowingOptions) {
26 | EmptyView()
27 | }
28 | if let username = viewModel.user.username {
29 | Text(username)
30 | .font(.title)
31 | .frame(alignment: .leading)
32 | .padding()
33 | }
34 | Spacer()
35 | if viewModel.user.objectId == User.current?.objectId {
36 | Button(action: {
37 | self.postStatus.isShowing = true
38 | }, label: {
39 | Image(systemName: "square.and.pencil")
40 | .resizable()
41 | .foregroundColor(Color(tintColor))
42 | .frame(width: 30, height: 30, alignment: .trailing)
43 | })
44 | Button(action: {
45 | self.isShowingOptions = true
46 | }, label: {
47 | Image(systemName: "slider.horizontal.3")
48 | .resizable()
49 | .foregroundColor(Color(tintColor))
50 | .frame(width: 30, height: 30, alignment: .trailing)
51 | .padding([.trailing])
52 | })
53 | }
54 | }
55 | }
56 | }
57 |
58 | struct ProfileHeaderView_Previews: PreviewProvider {
59 | static var previews: some View {
60 | ProfileHeaderView(viewModel: .init(user: User(),
61 | isShowingHeading: true),
62 | timeLineViewModel: .init(query: Post.query()))
63 | .environmentObject(PostStatus(isShowing: true))
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/SnapCat/Profile/ProfileUserDetailsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileUserDetailsView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/18/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ParseSwift
11 |
12 | struct ProfileUserDetailsView: View {
13 | @Environment(\.tintColor) private var tintColor
14 | @ObservedObject var viewModel: ProfileViewModel
15 | @ObservedObject var followersViewModel: QueryViewModel
16 | @ObservedObject var followingsViewModel: QueryViewModel
17 | @ObservedObject var timeLineViewModel: QueryImageViewModel
18 | @State var gradient = LinearGradient(gradient: Gradient(colors: [Color(#colorLiteral(red: 0, green: 0.2858072221, blue: 0.6897063851, alpha: 1)), Color(#colorLiteral(red: 0.06253327429, green: 0.6597633362, blue: 0.8644603491, alpha: 1))]),
19 | startPoint: .top,
20 | endPoint: .bottom)
21 | @State var explorerView: ExploreView?
22 | @State var isShowingImagePicker = false
23 | @State var isShowingEditProfile = false
24 | @State var isShowingExplorer = false
25 |
26 | var body: some View {
27 | VStack {
28 | NavigationLink(destination: self.explorerView
29 | .navigationBarHidden(false),
30 | isActive: $isShowingExplorer) {
31 | EmptyView()
32 | }
33 | HStack {
34 | Button(action: {
35 | self.isShowingImagePicker = true
36 | }, label: {
37 | if let image = viewModel.profilePicture {
38 | Image(uiImage: image)
39 | .resizable()
40 | .frame(idealWidth: 90, maxWidth: 90, idealHeight: 90, maxHeight: 90)
41 | .clipShape(Circle())
42 | .shadow(radius: 3)
43 | .overlay(Circle().stroke(gradient, lineWidth: 3))
44 | .padding()
45 | } else {
46 | Image(systemName: "person.circle")
47 | .resizable()
48 | .frame(idealWidth: 90, maxWidth: 90, idealHeight: 90, maxHeight: 90)
49 | .clipShape(Circle())
50 | .shadow(radius: 3)
51 | .overlay(Circle().stroke(Color(tintColor), lineWidth: 3))
52 | .padding()
53 | }
54 | })
55 | .buttonStyle(PlainButtonStyle())
56 | Spacer()
57 | VStack {
58 | Text("\(timeLineViewModel.results.count)")
59 | .padding(.trailing)
60 | Text("Posts")
61 | .padding(.trailing)
62 | }
63 | Button(action: {
64 | let explorerViewModel = ExploreViewModel(isShowingFollowers: true,
65 | followersViewModel: followersViewModel,
66 | followingsViewModel: followingsViewModel)
67 | self.explorerView = ExploreView(viewModel: explorerViewModel)
68 | self.isShowingExplorer = true
69 | }, label: {
70 | VStack {
71 | Text("\(followersViewModel.results.count)")
72 | .padding(.trailing)
73 | Text("Followers")
74 | .padding(.trailing)
75 | }
76 | })
77 | .buttonStyle(PlainButtonStyle())
78 | Button(action: {
79 | let explorerViewModel = ExploreViewModel(isShowingFollowers: false,
80 | followersViewModel: followersViewModel,
81 | followingsViewModel: followingsViewModel)
82 | self.explorerView = ExploreView(viewModel: explorerViewModel)
83 | self.isShowingExplorer = true
84 | }, label: {
85 | VStack {
86 | Text("\(followingsViewModel.results.count)")
87 | .padding(.trailing)
88 | Text("Following")
89 | .padding(.trailing)
90 | }
91 | })
92 | .buttonStyle(PlainButtonStyle())
93 | }
94 | HStack {
95 | VStack(alignment: .leading) {
96 | if let name = viewModel.user.name {
97 | Text(name)
98 | .padding([.leading])
99 | .font(.title2)
100 | }
101 | if let bio = viewModel.user.bio {
102 | Text(bio)
103 | .padding([.leading])
104 | }
105 | if let link = viewModel.user.link {
106 | Link(destination: link, label: {
107 | Text("\(link.absoluteString)")
108 | })
109 | .padding([.leading])
110 | }
111 | }
112 | Spacer()
113 | }
114 | if viewModel.user.objectId == User.current?.objectId {
115 | Button(action: {
116 | self.isShowingEditProfile = true
117 | }, label: {
118 | Text("Edit Profile")
119 | .frame(minWidth: 0, maxWidth: .infinity)
120 | .foregroundColor(.white)
121 | .padding()
122 | .cornerRadius(15)
123 | .background(Color(tintColor))
124 | })
125 | .padding([.leading, .trailing], 20)
126 | } else {
127 | if viewModel.isCurrentFollowing() {
128 | Button(action: {
129 | Task {
130 | await self.viewModel.unfollowUser()
131 | }
132 | }, label: {
133 | Text("Unfollow")
134 | .frame(minWidth: 0, maxWidth: .infinity)
135 | .foregroundColor(.white)
136 | .padding()
137 | .cornerRadius(15)
138 | .background(Color(#colorLiteral(red: 0.06253327429, green: 0.6597633362, blue: 0.8644603491, alpha: 1)))
139 | })
140 | .padding([.leading, .trailing], 20)
141 | } else {
142 | Button(action: {
143 | Task {
144 | await self.viewModel.followUser()
145 | }
146 | }, label: {
147 | Text("Follow")
148 | .frame(minWidth: 0, maxWidth: .infinity)
149 | .foregroundColor(.white)
150 | .padding()
151 | .cornerRadius(15)
152 | .background(Color(#colorLiteral(red: 0.7843137255, green: 0.7843137255, blue: 0.7843137255, alpha: 1)))
153 | })
154 | .padding([.leading, .trailing], 20)
155 | }
156 | }
157 | }.sheet(isPresented: $isShowingImagePicker, onDismiss: {}, content: {
158 | ImagePickerView(image: $viewModel.profilePicture)
159 | }).sheet(isPresented: $isShowingEditProfile, onDismiss: {}, content: {
160 | ProfileEditView(viewModel: viewModel)
161 | })
162 | }
163 | }
164 |
165 | struct ProfileUserDetailsView_Previews: PreviewProvider {
166 | static var previews: some View {
167 | ProfileUserDetailsView(viewModel: .init(user: User(), isShowingHeading: true),
168 | followersViewModel: .init(query: Activity.query()),
169 | followingsViewModel: .init(query: Activity.query()),
170 | timeLineViewModel: .init(query: Post.query()))
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/SnapCat/Profile/ProfileView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ParseSwift
11 |
12 | struct ProfileView: View {
13 | @Environment(\.tintColor) private var tintColor
14 | @ObservedObject var timeLineViewModel: QueryImageViewModel
15 | @ObservedObject var followersViewModel: QueryViewModel
16 | @ObservedObject var followingsViewModel: QueryViewModel
17 | @ObservedObject var viewModel: ProfileViewModel
18 |
19 | var body: some View {
20 | VStack {
21 | if viewModel.isShowingHeading {
22 | ProfileHeaderView(viewModel: viewModel,
23 | timeLineViewModel: timeLineViewModel)
24 | } else {
25 | HStack {
26 | if let username = viewModel.user.username {
27 | Text(username)
28 | .font(.title)
29 | .frame(alignment: .leading)
30 | .padding()
31 | }
32 | if viewModel.isCurrentFollower() {
33 | Label("Follows You",
34 | systemImage: "checkmark.square.fill")
35 | }
36 | Spacer()
37 | }
38 | }
39 | ProfileUserDetailsView(viewModel: viewModel,
40 | followersViewModel: followersViewModel,
41 | followingsViewModel: followingsViewModel,
42 | timeLineViewModel: timeLineViewModel)
43 | Divider()
44 | TimeLineView(viewModel: timeLineViewModel)
45 | }.onAppear(perform: {
46 | followersViewModel.find()
47 | followingsViewModel.find()
48 | })
49 | }
50 |
51 | init(user: User? = nil, isShowingHeading: Bool = true) {
52 | var userProfile: User!
53 | if let user = user {
54 | userProfile = user
55 | } else if let currentUser = User.current {
56 | userProfile = currentUser
57 | } else {
58 | userProfile = User()
59 | }
60 | viewModel = ProfileViewModel(user: userProfile,
61 | isShowingHeading: isShowingHeading)
62 | let timeLineQuery = ProfileViewModel
63 | .queryUserTimeLine(userProfile)
64 | .include(PostKey.user)
65 | if let timeLine = timeLineQuery.subscribeCustom {
66 | timeLineViewModel = timeLine
67 | } else {
68 | timeLineViewModel = timeLineQuery.imageViewModel
69 | }
70 | followersViewModel = ProfileViewModel
71 | .queryFollowers(userProfile)
72 | .include(ActivityKey.fromUser)
73 | .viewModel
74 | followingsViewModel = ProfileViewModel
75 | .queryFollowings(userProfile)
76 | .include(ActivityKey.toUser)
77 | .viewModel
78 | timeLineViewModel.find()
79 | followersViewModel.find()
80 | followingsViewModel.find()
81 | }
82 | }
83 |
84 | struct ProfileView_Previews: PreviewProvider {
85 | static var previews: some View {
86 | ProfileView()
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/SnapCat/Profile/ProfileViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileViewModel.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/4/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 | import ParseSwift
12 | import SwiftUI
13 | import UIKit
14 |
15 | class ProfileViewModel: ObservableObject { // swiftlint:disable:this type_body_length
16 | @Published var user = User()
17 | @Published var error: SnapCatError?
18 | var username: String = "" {
19 | willSet {
20 | if !isSettingForFirstTime {
21 | isHasChanges = true
22 | }
23 | objectWillChange.send()
24 | }
25 | }
26 | var email: String = "" {
27 | willSet {
28 | if !isSettingForFirstTime {
29 | isHasChanges = true
30 | }
31 | objectWillChange.send()
32 | }
33 | }
34 | var name: String = "" {
35 | willSet {
36 | if !isSettingForFirstTime {
37 | isHasChanges = true
38 | }
39 | objectWillChange.send()
40 | }
41 | }
42 | var bio: String = "" {
43 | willSet {
44 | if !isSettingForFirstTime {
45 | isHasChanges = true
46 | }
47 | objectWillChange.send()
48 | }
49 | }
50 | var link: String = "" {
51 | willSet {
52 | if !isSettingForFirstTime {
53 | isHasChanges = true
54 | }
55 | objectWillChange.send()
56 | }
57 | }
58 | @Published var isHasChanges = false
59 | @Published var currentUserFollowers = [User]()
60 | @Published var currentUserFollowings = [User]()
61 | @Published var isShowingHeading = true
62 | private var settingProfilePicForFirstTime = true
63 | var profilePicture = UIImage(systemName: "person.circle") {
64 | willSet {
65 | if !isSettingForFirstTime {
66 | guard var currentUser = User.current?.mergeable,
67 | currentUser.hasSameObjectId(as: user),
68 | let image = newValue,
69 | let compressed = image.compressTo(3) else {
70 | return
71 | }
72 | let newProfilePicture = ParseFile(name: "profile.jpeg", data: compressed)
73 | currentUser.profileImage = newProfilePicture
74 | let immutableCurrentUser = currentUser
75 | Task {
76 | do {
77 | let user = try await immutableCurrentUser.save()
78 | DispatchQueue.main.async {
79 | self.user = user
80 | }
81 | do {
82 | let fetchedUser = try await user.fetch()
83 | do {
84 | guard let cachedFile = try await fetchedUser
85 | .profileImage?
86 | .fetch() else {
87 | Logger.profile.error("Error fetching pic, couldn't unwrap")
88 | return
89 | }
90 | Logger.profile.info("Saved profile pic to cache: \(cachedFile, privacy: .private)")
91 | } catch {
92 | Logger.profile.error("Error fetching pic \(error.localizedDescription)")
93 | }
94 | } catch {
95 | Logger.profile.error("Error fetching profile pic from cloud: \(error.localizedDescription)")
96 | }
97 | } catch {
98 | guard let parseError = error as? ParseError else {
99 | return
100 | }
101 | Logger.profile.error("Error saving profile pic \(error.localizedDescription)")
102 | DispatchQueue.main.async {
103 | self.error = SnapCatError(parseError: parseError)
104 | }
105 | }
106 | }
107 | }
108 | objectWillChange.send()
109 | }
110 | }
111 | private var isSettingForFirstTime = true
112 |
113 | // swiftlint:disable:next function_body_length
114 | init(user: User?, isShowingHeading: Bool) {
115 | guard let currentUser = User.current else {
116 | Logger.profile.error("User should be logged in to perfom action.")
117 | return
118 | }
119 | DispatchQueue.main.async {
120 | if let user = user {
121 | self.user = user
122 | } else {
123 | self.user = currentUser
124 | }
125 | self.isShowingHeading = isShowingHeading
126 | if let username = self.user.username {
127 | self.username = username
128 | }
129 | if let email = self.user.email {
130 | self.email = email
131 | }
132 | if let name = self.user.name {
133 | self.name = name
134 | }
135 | if let bio = self.user.bio {
136 | self.bio = bio
137 | }
138 | if let link = self.user.link {
139 | self.link = link.absoluteString
140 | }
141 | self.isSettingForFirstTime = false
142 | DispatchQueue.main.async {
143 | Task {
144 | let image = await Utility.fetchImage(self.user.profileImage)
145 | self.isSettingForFirstTime = true
146 | self.profilePicture = image
147 | self.isSettingForFirstTime = false
148 | }
149 | }
150 | Task {
151 | do {
152 | let activities = try await Self.queryFollowers().find()
153 | self.currentUserFollowers = activities.compactMap { $0.fromUser }
154 | } catch {
155 | Logger.explore.error("Failed to query current followers: \(error.localizedDescription)")
156 | }
157 | }
158 | Task {
159 | do {
160 | let activities = try await Self.queryFollowings().find()
161 | self.currentUserFollowings = activities.compactMap { $0.toUser }
162 | } catch {
163 | Logger.explore.error("Failed to query current followings: \(error.localizedDescription)")
164 | }
165 | }
166 | }
167 | }
168 |
169 | // MARK: Intents
170 | func followUser() async {
171 | do {
172 | let newActivity = try Activity(type: .follow, from: User.current, to: user)
173 | .setupForFollowing()
174 | do {
175 | _ = try await newActivity.save()
176 | } catch {
177 | Logger.profile.error("Couldn't save follow: \(error.localizedDescription)")
178 | }
179 | } catch {
180 | Logger.profile.error("Can't create follow activity \(error.localizedDescription)")
181 | }
182 | }
183 |
184 | func unfollowUser() async {
185 | do {
186 | guard let currentUser = User.current else {
187 | return
188 | }
189 | let query = try Activity.query(ActivityKey.fromUser == currentUser,
190 | ActivityKey.toUser == user,
191 | ActivityKey.type == Activity.ActionType.follow)
192 | do {
193 | let activity = try await query.first()
194 | do {
195 | try await activity.delete()
196 | } catch {
197 | Logger.profile.error("Couldn't unfollow user \(error.localizedDescription)")
198 | }
199 |
200 | } catch {
201 | Logger.profile.error("Couldn't find activity to unfollow \(error.localizedDescription)")
202 | }
203 | } catch {
204 | Logger.profile.error("Couldn't unwrap during unfollow \(error.localizedDescription)")
205 | }
206 | }
207 |
208 | // MARK: Helpers
209 | func isCurrentFollower() -> Bool {
210 | currentUserFollowers.first(where: { $0.hasSameObjectId(as: user) }) != nil
211 | }
212 |
213 | func isCurrentFollowing() -> Bool {
214 | currentUserFollowings.first(where: { $0.hasSameObjectId(as: user) }) != nil
215 | }
216 |
217 | class func getUsersFromFollowers(_ activities: [Activity]) -> [User] {
218 | activities.compactMap { $0.fromUser }
219 | }
220 |
221 | class func getUsersFromFollowings(_ activities: [Activity]) -> [User] {
222 | activities.compactMap { $0.toUser }
223 | }
224 |
225 | // MARK: - Intents
226 | @MainActor
227 | func saveUpdates() async throws -> User {
228 | guard var currentUser = User.current?.mergeable else {
229 | let snapCatError = SnapCatError(message: "Trying to save when user isn't logged in")
230 | Logger.profile.error("\(snapCatError.message)")
231 | throw snapCatError
232 | }
233 | if !currentUser.hasSameObjectId(as: user) {
234 | let snapCatError = SnapCatError(message: "Trying to save when this isn't the logged in user")
235 | Logger.profile.error("\(snapCatError.message)")
236 | throw snapCatError
237 | }
238 | var changesNeedToBeSaved = false
239 | if username != user.username && !username.isEmpty {
240 | currentUser.username = username
241 | changesNeedToBeSaved = true
242 | }
243 | if email != user.email && !email.isEmpty {
244 | currentUser.email = email
245 | changesNeedToBeSaved = true
246 | }
247 | if name != user.name && !name.isEmpty {
248 | currentUser.name = name
249 | changesNeedToBeSaved = true
250 | }
251 | if bio != user.bio && !bio.isEmpty {
252 | currentUser.bio = bio
253 | changesNeedToBeSaved = true
254 | }
255 | if URL(string: link) != user.link && !link.isEmpty {
256 | currentUser.link = URL(string: link)
257 | changesNeedToBeSaved = true
258 | }
259 | if changesNeedToBeSaved {
260 | let user = try await currentUser.save()
261 | Logger.profile.info("User saved updates")
262 | self.user = user
263 | self.isHasChanges = false
264 | return user
265 | } else {
266 | let snapCatError = SnapCatError(message: "No new changes to save")
267 | Logger.profile.debug("\(snapCatError.message)")
268 | throw snapCatError
269 | }
270 | }
271 |
272 | func resetPassword() async throws {
273 | guard let email = User.current?.email else {
274 | let snapCatError = SnapCatError(message: "Need to save a valid email address before reseting password")
275 | self.error = snapCatError
276 | throw snapCatError
277 | }
278 | do {
279 | return try await User.passwordReset(email: email)
280 | } catch {
281 | guard let parseError = error as? ParseError else {
282 | return
283 | }
284 | let snapCatError = SnapCatError(parseError: parseError)
285 | self.error = snapCatError
286 | throw snapCatError
287 | }
288 | }
289 |
290 | // MARK: - Queries
291 | class func queryUserTimeLine(_ user: User?=nil) -> Query {
292 | let userPointer: Pointer!
293 | if let otherUser = user {
294 | guard let pointer = try? otherUser.toPointer() else {
295 | return Post.query().limit(0)
296 | }
297 | userPointer = pointer
298 | } else {
299 | guard let pointer = try? User.current?.toPointer() else {
300 | return Post.query().limit(0)
301 | }
302 | userPointer = pointer
303 | }
304 | return Post.query(PostKey.user == userPointer)
305 | .order([.descending(ParseKey.createdAt)])
306 | }
307 |
308 | class func queryFollowers(_ user: User?=nil) -> Query {
309 |
310 | var query = Activity.query().limit(0)
311 | if let currentUser = User.current {
312 | if let user = user {
313 | query = Activity.query(ActivityKey.toUser == user.objectId,
314 | ActivityKey.fromUser != user.objectId,
315 | ActivityKey.type == Activity.ActionType.follow.rawValue)
316 | .order([.descending(ParseKey.updatedAt)])
317 | } else {
318 | query = Activity.query(ActivityKey.toUser == currentUser.objectId,
319 | ActivityKey.fromUser != currentUser.objectId,
320 | ActivityKey.type == Activity.ActionType.follow.rawValue)
321 | .order([.descending(ParseKey.updatedAt)])
322 | }
323 | }
324 | return query
325 | }
326 |
327 | class func queryFollowings(_ user: User?=nil) -> Query {
328 |
329 | if let user = user {
330 | return Activity.query(ActivityKey.fromUser == user.objectId,
331 | ActivityKey.toUser != user.objectId,
332 | ActivityKey.type == Activity.ActionType.follow.rawValue)
333 | .order([.descending(ParseKey.updatedAt)])
334 | } else {
335 | guard let currentUser = User.current else {
336 | Logger.main.error("Utility.queryActivitiesForFollowings(), user not logged in.")
337 | return Activity.query().limit(0)
338 | }
339 | return Activity.query(ActivityKey.fromUser == currentUser.objectId,
340 | ActivityKey.toUser != currentUser.objectId,
341 | ActivityKey.type == Activity.ActionType.follow.rawValue)
342 | .order([.descending(ParseKey.updatedAt)])
343 | }
344 | }
345 | }
346 |
--------------------------------------------------------------------------------
/SnapCat/Profile/SettingsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsView.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/10/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import AuthenticationServices
11 |
12 | struct SettingsView: View {
13 |
14 | @Environment(\.tintColor) private var tintColor
15 | @EnvironmentObject var userStatus: UserStatus
16 | @StateObject var viewModel = SettingsViewModel()
17 |
18 | var body: some View {
19 | VStack {
20 | Spacer()
21 | if !User.apple.isLinked {
22 | SignInWithAppleButton(.continue,
23 | onRequest: { (request) in
24 | request.requestedScopes = [.fullName, .email]
25 | },
26 | onCompletion: { (result) in
27 |
28 | switch result {
29 | case .success(let authorization):
30 | Task {
31 | await viewModel.linkWithApple(authorization: authorization)
32 | }
33 | case .failure(let error):
34 | viewModel.linkError = SnapCatError(message: error.localizedDescription)
35 | }
36 | })
37 | .frame(width: 300, height: 50)
38 | .cornerRadius(15)
39 | }
40 | Button(action: {
41 | Task {
42 | await viewModel.logout()
43 | }
44 | }, label: {
45 | Text("Log out")
46 | .font(.headline)
47 | .foregroundColor(.white)
48 | .padding()
49 | .frame(width: 300, height: 50)
50 | })
51 | .background(Color(.red))
52 | .cornerRadius(15)
53 | Spacer()
54 | if let link = URL(string: "https://www.cs.uky.edu/~baker/"),
55 | let image = UIImage(named: "netrecon") {
56 | HStack {
57 | Spacer()
58 | Link(destination: link, label: {
59 | VStack {
60 | Text("Developed by the")
61 | .foregroundColor(Color(tintColor))
62 | Text("University of Kentucky")
63 | .foregroundColor(Color(tintColor))
64 | Image(uiImage: image)
65 | .resizable()
66 | .frame(width: 100, height: 50, alignment: .center)
67 | }
68 | })
69 | Spacer()
70 | }
71 | }
72 | }.onReceive(viewModel.$isLoggedOut, perform: { value in
73 | if self.userStatus.isLoggedOut != value {
74 | self.userStatus.check()
75 | }
76 | })
77 | .navigationBarTitle("Settings")
78 | .navigationBarHidden(false)
79 | }
80 | }
81 |
82 | struct SettingsView_Previews: PreviewProvider {
83 | static var previews: some View {
84 | SettingsView()
85 | .environmentObject(UserStatus(isLoggedOut: false))
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/SnapCat/Profile/SettingsViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsViewModel.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/10/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 | import ParseSwift
12 | import AuthenticationServices
13 |
14 | class SettingsViewModel: ObservableObject {
15 | @Published var isLoggedOut = false
16 | @Published var linkError: SnapCatError?
17 |
18 | // MARK: - Intents
19 | /**
20 | Links the user with Apple *asynchronously*.
21 | - parameter authorization: The encapsulation of a successful authorization performed by a controller..
22 | */
23 | @MainActor
24 | func linkWithApple(authorization: ASAuthorization) async {
25 | guard let credentials = authorization.credential as? ASAuthorizationAppleIDCredential,
26 | let identityToken = credentials.identityToken else {
27 | let error = "Failed unwrapping Apple authorization credentials."
28 | Logger.settings.error("Apple Login Error: \(error)")
29 | return
30 | }
31 |
32 | do {
33 | var user = try await User.apple.link(user: credentials.user,
34 | identityToken: identityToken)
35 | var isUpdatedUser = false
36 | if user.email == nil && user.email != nil {
37 | user.email = credentials.email
38 | isUpdatedUser = true
39 | }
40 | if user.name == nil {
41 | if let name = credentials.fullName {
42 | var currentName = ""
43 | if let givenName = name.givenName {
44 | currentName = givenName
45 | }
46 | if let familyName = name.familyName {
47 | if currentName != "" {
48 | currentName = "\(currentName) \(familyName)"
49 | } else {
50 | currentName = familyName
51 | }
52 | }
53 | user.name = currentName
54 | isUpdatedUser = true
55 | }
56 | }
57 | let loggedInUser: User!
58 | if isUpdatedUser {
59 | loggedInUser = try await user.save()
60 | } else {
61 | loggedInUser = user
62 | }
63 | Logger.settings.debug("Apple Linking Success: \(loggedInUser, privacy: .private)")
64 | } catch {
65 | guard let parseError = error as? ParseError else {
66 | Logger.settings.error("Apple Linking Error: \(error.localizedDescription)")
67 | return
68 | }
69 | Logger.settings.error("Apple Linking Error: \(parseError)")
70 | self.linkError = SnapCatError(parseError: parseError)
71 | }
72 | }
73 |
74 | func logout() async {
75 | do {
76 | _ = try await User.logout()
77 | Logger.settings.info("User logged out")
78 | self.isLoggedOut = true
79 | } catch {
80 | guard let parseError = error as? ParseError else {
81 | Logger.settings.error("Error logging out: \(error.localizedDescription)")
82 | return
83 | }
84 | Logger.settings.error("Error logging out: \(parseError.localizedDescription)")
85 | self.linkError = SnapCatError(parseError: parseError)
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/SnapCat/SnapCat.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 | com.apple.developer.applesignin
8 |
9 | Default
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/SnapCat/SnapCatApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SnapCatApp.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/3/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ParseSwift
11 | import os.log
12 |
13 | @main
14 | struct SnapCatApp: App {
15 |
16 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
17 | @Environment(\.scenePhase) private var scenePhase
18 |
19 | var body: some Scene {
20 | WindowGroup {
21 | MainView()
22 | }.onChange(of: scenePhase, perform: { _ in
23 |
24 | })
25 | }
26 |
27 | init() {
28 | Utility.setupServer()
29 | }
30 | }
31 |
32 | class AppDelegate: NSObject, UIApplicationDelegate {
33 | func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
34 | guard var currentInstallation = Installation.current?.mergeable else {
35 | return
36 | }
37 | currentInstallation.setDeviceToken(deviceToken)
38 | currentInstallation.save { _ in }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/SnapCat/SnapCatError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SnapCatError.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/3/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 |
12 | public struct SnapCatError: Swift.Error {
13 |
14 | public let message: String
15 |
16 | public var localizedDescription: String {
17 | return "AssuageError error=\(message)"
18 | }
19 | }
20 |
21 | extension SnapCatError {
22 | init(parseError: ParseError) {
23 | message = parseError.description
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/SnapCat/Utility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utility.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 7/3/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 | import os.log
12 | import UIKit
13 |
14 | struct Utility {
15 |
16 | @MainActor
17 | static func fetchImage(_ file: ParseFile?) async -> UIImage? {
18 | let defaultImage = UIImage(systemName: "camera")
19 | guard let file = file else {
20 | return defaultImage
21 | }
22 | do {
23 | let image = try await file.fetch()
24 | if let path = image.localURL?.relativePath,
25 | let image = UIImage(contentsOfFile: path) {
26 | return image
27 | } else {
28 | return defaultImage
29 | }
30 | } catch {
31 | Logger.home.error("Error fetching picture: \(error.localizedDescription)")
32 | return defaultImage
33 | }
34 | }
35 |
36 | static func removeFilesAtDirectory(_ originalPath: String, isDirectory: Bool) throws {
37 | var path = URL(fileURLWithPath: originalPath, isDirectory: true)
38 | if !isDirectory {
39 | var pathArray = originalPath.components(separatedBy: "/")
40 | pathArray.removeFirst()
41 | pathArray.removeLast()
42 | path = URL(fileURLWithPath: "")
43 | pathArray.forEach {
44 | path.appendPathComponent($0, isDirectory: true)
45 | }
46 | }
47 |
48 | let contents = try FileManager.default.contentsOfDirectory(atPath: path.path)
49 | if contents.count == 0 {
50 | return
51 | }
52 | try contents.forEach {
53 | let filePath = path.appendingPathComponent($0)
54 | try FileManager.default.removeItem(at: filePath)
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/SnapCat/ViewModels/PostStatus.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostStatus.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 1/8/22.
6 | // Copyright © 2022 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class PostStatus: ObservableObject {
12 | @Published var isShowing: Bool
13 |
14 | init(isShowing: Bool = false) {
15 | self.isShowing = isShowing
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/SnapCat/ViewModels/UserStatus.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserStatus.swift
3 | // SnapCat
4 | //
5 | // Created by Corey Baker on 1/8/22.
6 | // Copyright © 2022 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class UserStatus: ObservableObject {
12 | @Published var isLoggedOut = true
13 |
14 | init() {
15 | check()
16 | }
17 |
18 | init(isLoggedOut: Bool) {
19 | self.isLoggedOut = isLoggedOut
20 | }
21 |
22 | func check() {
23 | if User.current != nil {
24 | isLoggedOut = false
25 | } else {
26 | isLoggedOut = true
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/SnapCatTests/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 |
--------------------------------------------------------------------------------
/SnapCatTests/SnapCatTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SnapCatTests.swift
3 | // SnapCatTests
4 | //
5 | // Created by Corey Baker on 7/3/21.
6 | //
7 |
8 | import XCTest
9 | @testable import SnapCat
10 |
11 | class SnapCatTests: 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 |
--------------------------------------------------------------------------------
/SnapCatUITests/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 |
--------------------------------------------------------------------------------
/SnapCatUITests/SnapCatUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SnapCatUITests.swift
3 | // SnapCatUITests
4 | //
5 | // Created by Corey Baker on 7/3/21.
6 | //
7 |
8 | import XCTest
9 |
10 | class SnapCatUITests: 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 |
19 | override func tearDownWithError() throws {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | }
22 |
23 | func testExample() throws {
24 | // UI tests must launch the application that they test.
25 | let app = XCUIApplication()
26 | app.launch()
27 |
28 | // Use recording to get started writing UI tests.
29 | // Use XCTAssert and related functions to verify your tests produce the correct results.
30 | }
31 |
32 | func testLaunchPerformance() throws {
33 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
34 | // This measures how long it takes to launch your application.
35 | measure(metrics: [XCTApplicationLaunchMetric()]) {
36 | XCUIApplication().launch()
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------