├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .swiftlint.yml
├── CONTRIBUTING.md
├── FriendlyPix.xcodeproj
├── project.pbxproj
└── xcshareddata
│ └── xcschemes
│ └── FriendlyPix.xcscheme
├── FriendlyPix
├── Amaranth-Bold.otf
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon-App-20x20@1x.png
│ │ ├── Icon-App-20x20@2x.png
│ │ ├── Icon-App-20x20@3x.png
│ │ ├── Icon-App-29x29@1x.png
│ │ ├── Icon-App-29x29@2x.png
│ │ ├── Icon-App-29x29@3x.png
│ │ ├── Icon-App-40x40@1x.png
│ │ ├── Icon-App-40x40@2x.png
│ │ ├── Icon-App-40x40@3x.png
│ │ ├── Icon-App-57x57@1x.png
│ │ ├── Icon-App-57x57@2x.png
│ │ ├── Icon-App-60x60@2x.png
│ │ ├── Icon-App-60x60@3x.png
│ │ ├── Icon-App-72x72@1x.png
│ │ ├── Icon-App-72x72@2x.png
│ │ ├── Icon-App-76x76@1x.png
│ │ ├── Icon-App-76x76@2x.png
│ │ ├── Icon-App-83.5x83.5@2x.png
│ │ ├── Icon-Small-50x50@1x.png
│ │ ├── Icon-Small-50x50@2x.png
│ │ └── ItunesArtwork@2x.png
│ ├── Contents.json
│ ├── Logo.imageset
│ │ ├── Contents.json
│ │ ├── logo1024-universal-341@1x.png
│ │ ├── logo1024-universal-341@2x.png
│ │ └── logo1024-universal-341@3x.png
│ ├── friendlypix_launch_logo.imageset
│ │ ├── 144_1x.png
│ │ ├── 144_2x.png
│ │ ├── 144_3x.png
│ │ ├── 192_1x.png
│ │ ├── 192_2x.png
│ │ └── Contents.json
│ ├── google_launch_logo.imageset
│ │ ├── Contents.json
│ │ ├── googlelogo_dark20_color_132x44pt_1x.png
│ │ ├── googlelogo_dark20_color_132x44pt_2x.png
│ │ ├── googlelogo_dark20_color_132x44pt_3x.png
│ │ ├── googlelogo_dark20_color_184x60pt_1x.png
│ │ └── googlelogo_dark20_color_184x60pt_2x.png
│ ├── ic_account_circle_36pt.imageset
│ │ ├── Contents.json
│ │ ├── ic_account_circle_36pt.png
│ │ ├── ic_account_circle_36pt_2x.png
│ │ └── ic_account_circle_36pt_3x.png
│ ├── ic_arrow_back.imageset
│ │ ├── Contents.json
│ │ ├── ic_arrow_back.png
│ │ ├── ic_arrow_back_2x.png
│ │ └── ic_arrow_back_3x.png
│ ├── ic_close.imageset
│ │ ├── Contents.json
│ │ ├── ic_close.png
│ │ ├── ic_close_2x.png
│ │ └── ic_close_3x.png
│ ├── ic_comment.imageset
│ │ ├── Contents.json
│ │ ├── ic_comment_36pt.png
│ │ ├── ic_comment_36pt_2x.png
│ │ └── ic_comment_36pt_3x.png
│ ├── ic_comment_white.imageset
│ │ ├── Contents.json
│ │ ├── ic_comment_white_36pt.png
│ │ ├── ic_comment_white_36pt_2x.png
│ │ └── ic_comment_white_36pt_3x.png
│ ├── ic_delete_forever_white.imageset
│ │ ├── Contents.json
│ │ ├── ic_delete_forever_white.png
│ │ ├── ic_delete_forever_white_2x.png
│ │ └── ic_delete_forever_white_3x.png
│ ├── ic_favorite.imageset
│ │ ├── Contents.json
│ │ ├── ic_favorite_36pt.png
│ │ ├── ic_favorite_36pt_2x.png
│ │ └── ic_favorite_36pt_3x.png
│ ├── ic_favorite_border.imageset
│ │ ├── Contents.json
│ │ ├── ic_favorite_border_36pt.png
│ │ ├── ic_favorite_border_36pt_2x.png
│ │ └── ic_favorite_border_36pt_3x.png
│ ├── ic_home.imageset
│ │ ├── Contents.json
│ │ ├── ic_home.png
│ │ ├── ic_home_2x.png
│ │ └── ic_home_3x.png
│ ├── ic_logout.imageset
│ │ ├── Contents.json
│ │ ├── ic_logout_black_1x_ios_24dp.png
│ │ ├── ic_logout_black_2x_ios_24dp.png
│ │ └── ic_logout_black_3x_ios_24dp.png
│ ├── ic_more_vert.imageset
│ │ ├── Contents.json
│ │ ├── ic_more_vert.png
│ │ ├── ic_more_vert_2x.png
│ │ └── ic_more_vert_3x.png
│ ├── ic_more_vert_white.imageset
│ │ ├── Contents.json
│ │ ├── ic_more_vert_white.png
│ │ ├── ic_more_vert_white_2x.png
│ │ └── ic_more_vert_white_3x.png
│ ├── ic_photo_camera.imageset
│ │ ├── Contents.json
│ │ ├── ic_photo_camera.png
│ │ ├── ic_photo_camera_2x.png
│ │ └── ic_photo_camera_3x.png
│ ├── ic_photo_camera_white.imageset
│ │ ├── Contents.json
│ │ ├── ic_photo_camera_white.png
│ │ ├── ic_photo_camera_white_2x.png
│ │ └── ic_photo_camera_white_3x.png
│ ├── ic_search.imageset
│ │ ├── Contents.json
│ │ ├── ic_search.png
│ │ ├── ic_search_2x.png
│ │ └── ic_search_3x.png
│ ├── ic_send.imageset
│ │ ├── Contents.json
│ │ ├── ic_send.png
│ │ ├── ic_send_2x.png
│ │ └── ic_send_3x.png
│ ├── ic_trending_up.imageset
│ │ ├── Contents.json
│ │ ├── ic_trending_up.png
│ │ ├── ic_trending_up_2x.png
│ │ └── ic_trending_up_3x.png
│ └── image_logo.imageset
│ │ ├── Contents.json
│ │ ├── Icon-App-29x29@1x.png
│ │ ├── Icon-App-29x29@2x.png
│ │ └── Icon-App-29x29@3x.png
├── Base.lproj
│ └── Main.storyboard
├── DateExtension.swift
├── FPAccountHeader.swift
├── FPAccountViewController.swift
├── FPAuthPickerViewController.swift
├── FPAuthPickerViewController.xib
├── FPCardCollectionViewCell.swift
├── FPCardCollectionViewCell.xib
├── FPCollectionViewTextCell.swift
├── FPComment.swift
├── FPCommentCell.swift
├── FPCommentViewController.swift
├── FPFeedViewController.swift
├── FPHashTagViewController.swift
├── FPPost.swift
├── FPPostDetailViewController.swift
├── FPSearchViewController.swift
├── FPUploadViewController.swift
├── FPUser.swift
├── FriendlyPix.entitlements
├── Launch.storyboard
├── UIImage+Circle.swift
└── UITextViewPlaceholder.swift
├── FriendlyPixTests
├── FriendlyPixTests.swift
└── Info.plist
├── FriendlyPixUITests
├── FriendlyPixUITests.swift
└── Info.plist
├── Gemfile
├── Gemfile.lock
├── Info.plist
├── LICENSE
├── Podfile
├── Podfile.lock
├── README.md
├── friendly-pix.png
├── info_script.rb
├── mock-GoogleService-Info.plist
└── test.sh
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | workflow_dispatch:
10 |
11 | jobs:
12 | build:
13 | runs-on: macos-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 |
18 | # Runs a set of commands using the runners shell
19 | - name: Run a multi-line script
20 | run: |
21 | bundle install
22 | gem install xcpretty
23 | gem install xcodeproj
24 | bundle exec pod install --repo-update
25 | cp ./mock-GoogleService-Info.plist GoogleService-Info.plist
26 | sed -i '' 's/YOUR_REVERSED_CLIENT_ID/com.googleusercontent.apps.123456789000-hjugbg6ud799v4c49dim8ce2usclthar/' Info.plist
27 | ruby ./info_script.rb
28 | ./test.sh
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | Pods/
3 | xcuserdata/
4 | *.xcworkspace/
5 | GoogleService-Info.plist
6 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | opt_in_rules:
2 | - empty_count
3 | - file_header
4 | - explicit_init
5 | - closure_spacing
6 | - overridden_super_call
7 | - redundant_nil_coalescing
8 | - private_outlet
9 | - nimble_operator
10 | - attributes
11 | - operator_usage_whitespace
12 | - closure_end_indentation
13 | - first_where
14 | - sorted_imports
15 | - object_literal
16 | - number_separator
17 | - prohibited_super_call
18 | - fatal_error_message
19 | - vertical_parameter_alignment_on_call
20 | - let_var_whitespace
21 | - unneeded_parentheses_in_closure_argument
22 | - extension_access_modifier
23 | - pattern_matching_keywords
24 | - array_init
25 | - literal_expression_end_indentation
26 |
27 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Friendly Pix
2 |
3 | We'd love for you to contribute to our source code and to make the Friendly Pix even better than it is today! Here are the guidelines we'd like you to follow:
4 |
5 | - [Code of Conduct](#coc)
6 | - [Question or Problem?](#question)
7 | - [Issues and Bugs](#issue)
8 | - [Feature Requests](#feature)
9 | - [Submission Guidelines](#submit)
10 | - [Coding Rules](#rules)
11 | - [Signing the CLA](#cla)
12 |
13 | ## Code of Conduct
14 |
15 | As contributors and maintainers of the Friendly Pix project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities.
16 |
17 | Communication through any of Friendly Pix's channels (GitHub, StackOverflow, Google+, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
18 |
19 | We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the project to do the same.
20 |
21 | If any member of the community violates this code of conduct, the maintainers of the Friendly Pix project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate.
22 |
23 | If you are subject to or witness unacceptable behavior, or have any other concerns, please drop us a line at nivco@google.com.
24 |
25 | ## Got a Question or Problem?
26 |
27 | If you have technical questions about Friendly Pix, please direct these to [StackOverflow][stackoverflow] and use the `friendly-pix` tag. We are also available on GitHub issues.
28 |
29 | ## Found an Issue?
30 | If you find a bug in the source code or a mistake in the documentation, you can help us by
31 | submitting an issue to our [GitHub Repository][github]. Even better you can submit a Pull Request
32 | with a fix.
33 |
34 | See [below](#submit) for some guidelines.
35 |
36 | ## Want a Feature?
37 | You can request a new feature by submitting an issue to our [GitHub Repository][github].
38 |
39 | If you would like to implement a new feature then consider what kind of change it is:
40 |
41 | * **Major Changes** that you wish to contribute to the project should be discussed first on our
42 | [issue tracker][github] so that we can better coordinate our efforts, prevent
43 | duplication of work, and help you to craft the change so that it is successfully accepted into the
44 | project.
45 | * **Small Changes** can be crafted and submitted to the [GitHub Repository][github] as a Pull Request directly.
46 |
47 | ## Submission Guidelines
48 |
49 | ### Submitting an Issue
50 | Before you submit your issue search the archive, maybe your question was already answered.
51 |
52 | If your issue appears to be a bug, and hasn't been reported, open a new issue.
53 | Help us to maximize the effort we can spend fixing issues and adding new
54 | features, by not reporting duplicate issues. Providing the following information will increase the
55 | chances of your issue being dealt with quickly:
56 |
57 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps
58 | * **Motivation for or Use Case** - explain why this is a bug for you
59 | * **Friendly Pix Version(s)** - is it a regression?
60 | * **Browsers and Operating System** - is this a problem with all browsers or only some browsers?
61 | * **Reproduce the Error** - provide an unambiguous set of steps.
62 | * **Related Issues** - has a similar issue been reported before?
63 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
64 | causing the problem (line of code or commit)
65 |
66 | **If you get help, help others. Good karma rulez!**
67 |
68 | Here's a template to get you started:
69 |
70 | ```
71 | Browser:
72 | Browser version:
73 | Operating system:
74 | Operating system version:
75 | URL, if applicable:
76 |
77 | What steps will reproduce the problem:
78 | 1.
79 | 2.
80 | 3.
81 |
82 | What is the expected result?
83 |
84 | What happens instead of that?
85 |
86 | Please provide any other information below, and attach a screenshot if possible.
87 | ```
88 |
89 | ### Submitting a Pull Request
90 | Before you submit your pull request consider the following guidelines:
91 |
92 | * Search [GitHub](https://github.com/firebase/friendlypix/pulls) for an open or closed Pull Request
93 | that relates to your submission. You don't want to duplicate effort.
94 | * Please sign our [Contributor License Agreement (CLA)](#cla) before sending pull
95 | requests. We cannot accept code without this.
96 | * Make your changes in a new git branch:
97 |
98 | ```shell
99 | git checkout -b my-fix-branch master
100 | ```
101 |
102 | * Create your patch, **including appropriate test cases**.
103 | * Follow our [Coding Rules](#rules).
104 | * Avoid checking in files that shouldn't be tracked (e.g `node_modules`, `gulp-cache`, `.tmp`, `.idea`). We recommend using a [global](#global-gitignore) gitignore for this.
105 | * Make sure **not** to include a recompiled version of the files as part of your PR. We will generate these automatically.
106 | * Commit your changes using a descriptive commit message.
107 |
108 | ```shell
109 | git commit -a
110 | ```
111 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
112 |
113 | * Build your changes locally to ensure all the tests pass:
114 |
115 | ```shell
116 | gulp
117 | ```
118 |
119 | * Push your branch to GitHub:
120 |
121 | ```shell
122 | git push origin my-fix-branch
123 | ```
124 |
125 | * In GitHub, send a pull request to `friendlypix:master`.
126 | * If we suggest changes then:
127 | * Make the required updates.
128 | * Re-run the Friendly Pix test suite to ensure tests are still passing.
129 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
130 |
131 | ```shell
132 | git rebase master -i
133 | git push origin my-fix-branch -f
134 | ```
135 |
136 | That's it! Thank you for your contribution!
137 |
138 | #### After your pull request is merged
139 |
140 | After your pull request is merged, you can safely delete your branch and pull the changes
141 | from the main (upstream) repository:
142 |
143 | * Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows:
144 |
145 | ```shell
146 | git push origin --delete my-fix-branch
147 | ```
148 |
149 | * Check out the master branch:
150 |
151 | ```shell
152 | git checkout master -f
153 | ```
154 |
155 | * Delete the local branch:
156 |
157 | ```shell
158 | git branch -D my-fix-branch
159 | ```
160 |
161 | * Update your master with the latest upstream version:
162 |
163 | ```shell
164 | git pull --ff upstream master
165 | ```
166 |
167 | ## Coding Rules
168 |
169 | We generally follow the [Google JavaScript style guide][js-style-guide] for the Web version.
170 |
171 | ## Signing the CLA
172 |
173 | Please sign our [Contributor License Agreement][google-cla] (CLA) before sending pull requests. For any code
174 | changes to be accepted, the CLA must be signed. It's a quick process, we promise!
175 |
176 | *This guide was inspired by the [AngularJS contribution guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md).*
177 |
178 | [github]: https://github.com/firebase/friendlypix
179 | [google-cla]: https://cla.developers.google.com
180 | [js-style-guide]: http://google.github.io/styleguide/javascriptguide.xml
181 | [jsbin]: http://jsbin.com/
182 | [stackoverflow]: http://stackoverflow.com/questions/tagged/friendly-pix
183 | [global-gitignore]: https://help.github.com/articles/ignoring-files/#create-a-global-gitignore
184 |
--------------------------------------------------------------------------------
/FriendlyPix.xcodeproj/xcshareddata/xcschemes/FriendlyPix.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
42 |
48 |
49 |
50 |
52 |
58 |
59 |
60 |
61 |
62 |
72 |
74 |
80 |
81 |
82 |
83 |
87 |
88 |
89 |
92 |
93 |
94 |
100 |
102 |
108 |
109 |
110 |
111 |
113 |
114 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/FriendlyPix/Amaranth-Bold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Amaranth-Bold.otf
--------------------------------------------------------------------------------
/FriendlyPix/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Firebase
18 | import FirebaseUI
19 | import MaterialComponents
20 | import UserNotifications
21 |
22 | private let kFirebaseTermsOfService = URL(string: "https://firebase.google.com/terms/")!
23 |
24 | @UIApplicationMain
25 | class AppDelegate: UIResponder, UIApplicationDelegate {
26 |
27 | let mdcMessage = MDCSnackbarMessage()
28 | let mdcSnackBarManager = MDCSnackbarManager()
29 | let mdcAction = MDCSnackbarMessageAction()
30 | var window: UIWindow?
31 | lazy var database = Database.database()
32 | var blockedRef: DatabaseReference!
33 | var blockingRef: DatabaseReference!
34 | let gcmMessageIDKey = "gcm.message_id"
35 | var notificationGranted = false
36 | private var blocked = Set()
37 | private var blocking = Set()
38 | static var euroZone: Bool = {
39 | switch Locale.current.regionCode {
40 | case "CH", "AT", "IT", "BE", "LV", "BG", "LT", "HR", "LX", "CY", "MT", "CZ", "NL", "DK",
41 | "PL", "EE", "PT", "FI", "RO", "FR", "SK", "DE", "SI", "GR", "ES", "HU", "SE", "IE", "GB":
42 | return true
43 | default:
44 | return false
45 | }
46 | }()
47 |
48 | func application(_ application: UIApplication, didFinishLaunchingWithOptions
49 | launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
50 | FirebaseApp.configure()
51 | Messaging.messaging().delegate = self
52 | if let uid = Auth.auth().currentUser?.uid {
53 | blockedRef = database.reference(withPath: "blocked/\(uid)")
54 | blockingRef = database.reference(withPath: "blocking/\(uid)")
55 | observeBlocks()
56 | }
57 |
58 | if #available(iOS 10.0, *) {
59 | // For iOS 10 display notification (sent via APNS)
60 | UNUserNotificationCenter.current().delegate = self
61 |
62 | let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
63 | UNUserNotificationCenter.current().requestAuthorization(
64 | options: authOptions,
65 | completionHandler: { granted, _ in
66 | if granted {
67 | if let uid = Auth.auth().currentUser?.uid {
68 | self.database.reference(withPath: "people/\(uid)/notificationEnabled").setValue(true)
69 | } else {
70 | self.notificationGranted = true
71 | }
72 | }
73 | })
74 | } else {
75 | let settings: UIUserNotificationSettings =
76 | UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
77 | application.registerUserNotificationSettings(settings)
78 | }
79 |
80 | application.registerForRemoteNotifications()
81 |
82 | let authUI = FUIAuth.defaultAuthUI()
83 | authUI?.delegate = self
84 | authUI?.tosurl = kFirebaseTermsOfService
85 | authUI?.shouldAutoUpgradeAnonymousUsers = true
86 |
87 | let providers: [FUIAuthProvider] = AppDelegate.euroZone ? [FUIAnonymousAuth()] : [FUIGoogleAuth(), FUIAnonymousAuth()]
88 | // ((Auth.auth().currentUser != nil) ? [FUIGoogleAuth()] as [FUIAuthProvider] : [FUIGoogleAuth(), FUIAnonymousAuth()//, FUIFacebookAuth
89 | //] as [FUIAuthProvider])
90 | authUI?.providers = providers
91 | return true
92 | }
93 |
94 | func showAlert(_ userInfo: [AnyHashable: Any]) {
95 | let apsKey = "aps"
96 | let gcmMessage = "alert"
97 | let gcmLabel = "google.c.a.c_l"
98 | if let aps = userInfo[apsKey] as? [String: String], !aps.isEmpty, let message = aps[gcmMessage],
99 | let label = userInfo[gcmLabel] as? String {
100 | mdcMessage.text = "\(label): \(message)"
101 |
102 | mdcSnackBarManager.show(mdcMessage)
103 | }
104 | }
105 |
106 | func showContent(_ content: UNNotificationContent) {
107 | mdcMessage.text = content.body
108 | mdcAction.title = content.title
109 | mdcMessage.duration = 10_000
110 | mdcAction.handler = {
111 | guard let feed = self.window?.rootViewController?.children[0] as? FPFeedViewController else { return }
112 | let userId = content.categoryIdentifier.components(separatedBy: "/user/")[1]
113 | feed.showProfile(FPUser(dictionary: ["uid": userId]))
114 | }
115 | mdcMessage.action = mdcAction
116 | mdcSnackBarManager.show(mdcMessage)
117 | }
118 |
119 | @available(iOS 9.0, *)
120 | func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
121 | guard let sourceApplication = options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String else {
122 | return false
123 | }
124 | return self.handleOpenUrl(url, sourceApplication: sourceApplication)
125 | }
126 |
127 | @available(iOS 8.0, *)
128 | func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
129 | return self.handleOpenUrl(url, sourceApplication: sourceApplication)
130 | }
131 |
132 | func handleOpenUrl(_ url: URL, sourceApplication: String?) -> Bool {
133 | return FUIAuth.defaultAuthUI()?.handleOpen(url, sourceApplication: sourceApplication) ?? false
134 | }
135 |
136 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
137 | // If you are receiving a notification message while your app is in the background,
138 | // this callback will not be fired till the user taps on the notification launching the application.
139 | showAlert(userInfo)
140 | }
141 |
142 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
143 | fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
144 | // If you are receiving a notification message while your app is in the background,
145 | // this callback will not be fired till the user taps on the notification launching the application.
146 | showAlert(userInfo)
147 | completionHandler(.newData)
148 | }
149 |
150 | func observeBlocks() {
151 | blockedRef.observe(.childAdded) { self.blocked.insert($0.key) }
152 | blockingRef.observe(.childAdded) { self.blocking.insert($0.key) }
153 | blockedRef.observe(.childRemoved) { self.blocked.remove($0.key) }
154 | blockingRef.observe(.childRemoved) { self.blocking.remove($0.key) }
155 | }
156 |
157 | func isBlocked(_ snapshot: DataSnapshot) -> Bool {
158 | let author = snapshot.childSnapshot(forPath: "author/uid").value as! String
159 | if blocked.contains(author) || blocking.contains(author) {
160 | return true
161 | }
162 | return false
163 | }
164 |
165 | func isBlocked(by person: String) -> Bool {
166 | return blocked.contains(person)
167 | }
168 |
169 | func isBlocking(_ person: String) -> Bool {
170 | return blocking.contains(person)
171 | }
172 | }
173 |
174 | extension AppDelegate: FUIAuthDelegate {
175 | func authUI(_ authUI: FUIAuth, didSignInWith authDataResult: AuthDataResult?, error: Error?) {
176 | switch error {
177 | case .some(let error as NSError) where UInt(error.code) == FUIAuthErrorCode.userCancelledSignIn.rawValue:
178 | print("User cancelled sign-in")
179 | case .some(let error as NSError) where UInt(error.code) == FUIAuthErrorCode.mergeConflict.rawValue:
180 | mdcSnackBarManager.show(MDCSnackbarMessage(text: "This identity is already associated with a different user account."))
181 | case .some(let error as NSError) where UInt(error.code) == FUIAuthErrorCode.providerError.rawValue:
182 | mdcSnackBarManager.show(MDCSnackbarMessage(text: "There is an error with Google Sign in."))
183 | case .some(let error as NSError) where error.userInfo[NSUnderlyingErrorKey] != nil:
184 | mdcSnackBarManager.show(MDCSnackbarMessage(text: "\(error.userInfo[NSUnderlyingErrorKey]!)"))
185 | case .some(let error):
186 | mdcSnackBarManager.show(MDCSnackbarMessage(text: error.localizedDescription))
187 | case .none:
188 | if let user = authDataResult?.user {
189 | signed(in: user)
190 | }
191 | }
192 | }
193 |
194 | func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
195 | return FPAuthPickerViewController(nibName: "FPAuthPickerViewController", bundle: Bundle.main, authUI: authUI)
196 | }
197 |
198 | func signOut() {
199 | blockedRef.removeAllObservers()
200 | blockingRef.removeAllObservers()
201 | blocked.removeAll()
202 | blocking.removeAll()
203 | }
204 |
205 | func signed(in user: User) {
206 | blockedRef = database.reference(withPath: "blocked/\(user.uid)")
207 | blockingRef = database.reference(withPath: "blocking/\(user.uid)")
208 | observeBlocks()
209 | let imageUrl = user.isAnonymous ? "" : user.providerData[0].photoURL?.absoluteString
210 |
211 | // If the main profile Pic is an expiring facebook profile pic URL we'll update it automatically to use the permanent graph API URL.
212 | // if let url = imageUrl, url.contains("lookaside.facebook.com") || url.contains("fbcdn.net") {
213 | // let facebookUID = user.providerData.first { (userinfo) -> Bool in
214 | // return userinfo.providerID == "facebook.com"
215 | // }?.providerID
216 | // if let facebook = facebookUID {
217 | // imageUrl = "https://graph.facebook.com/\(facebook)/picture?type=large"
218 | // }
219 | // }
220 | let displayName = user.isAnonymous ? "Anonymous" : user.providerData[0].displayName ?? ""
221 |
222 |
223 | var values: [String: Any] = ["profile_picture": imageUrl ?? "",
224 | "full_name": displayName]
225 |
226 | if !user.isAnonymous, let name = user.providerData[0].displayName, !name.isEmpty {
227 | values["_search_index"] = ["full_name": name.lowercased(),
228 | "reversed_full_name": name.components(separatedBy: " ")
229 | .reversed().joined(separator: "")]
230 | }
231 |
232 | if notificationGranted {
233 | values["notificationEnabled"] = true
234 | notificationGranted = false
235 | }
236 | database.reference(withPath: "people/\(user.uid)")
237 | .updateChildValues(values)
238 | }
239 | }
240 |
241 | @available(iOS 10, *)
242 | extension AppDelegate: UNUserNotificationCenterDelegate {
243 |
244 | // Receive displayed notifications for iOS 10 devices.
245 | func userNotificationCenter(_ center: UNUserNotificationCenter,
246 | willPresent notification: UNNotification,
247 | withCompletionHandler completionHandler:
248 | @escaping (UNNotificationPresentationOptions) -> Void) {
249 | showContent(notification.request.content)
250 | completionHandler([])
251 | }
252 |
253 | func userNotificationCenter(_ center: UNUserNotificationCenter,
254 | didReceive response: UNNotificationResponse,
255 | withCompletionHandler completionHandler: @escaping () -> Void) {
256 | showContent(response.notification.request.content)
257 | completionHandler()
258 | }
259 | }
260 |
261 | extension AppDelegate: MessagingDelegate {
262 | func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) {
263 | guard let uid = Auth.auth().currentUser?.uid else { return }
264 | Database.database().reference(withPath: "/people/\(uid)/notificationTokens/\(fcmToken)").setValue(true)
265 | }
266 |
267 | // Receive data messages on iOS 10+ directly from FCM (bypassing APNs) when the app is in the foreground.
268 | // To enable direct data messages, you can set Messaging.messaging().shouldEstablishDirectChannel to true.
269 | func messaging(_ messaging: Messaging, didReceive remoteMessage: MessagingRemoteMessage) {
270 | let data = remoteMessage.appData
271 | showAlert(data)
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "57x57",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-57x57@1x.png",
49 | "scale" : "1x"
50 | },
51 | {
52 | "size" : "57x57",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-57x57@2x.png",
55 | "scale" : "2x"
56 | },
57 | {
58 | "size" : "60x60",
59 | "idiom" : "iphone",
60 | "filename" : "Icon-App-60x60@2x.png",
61 | "scale" : "2x"
62 | },
63 | {
64 | "size" : "60x60",
65 | "idiom" : "iphone",
66 | "filename" : "Icon-App-60x60@3x.png",
67 | "scale" : "3x"
68 | },
69 | {
70 | "size" : "20x20",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-20x20@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "20x20",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-20x20@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "29x29",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-29x29@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "29x29",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-29x29@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "40x40",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-40x40@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "40x40",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-40x40@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "50x50",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-Small-50x50@1x.png",
109 | "scale" : "1x"
110 | },
111 | {
112 | "size" : "50x50",
113 | "idiom" : "ipad",
114 | "filename" : "Icon-Small-50x50@2x.png",
115 | "scale" : "2x"
116 | },
117 | {
118 | "size" : "72x72",
119 | "idiom" : "ipad",
120 | "filename" : "Icon-App-72x72@1x.png",
121 | "scale" : "1x"
122 | },
123 | {
124 | "size" : "72x72",
125 | "idiom" : "ipad",
126 | "filename" : "Icon-App-72x72@2x.png",
127 | "scale" : "2x"
128 | },
129 | {
130 | "size" : "76x76",
131 | "idiom" : "ipad",
132 | "filename" : "Icon-App-76x76@1x.png",
133 | "scale" : "1x"
134 | },
135 | {
136 | "size" : "76x76",
137 | "idiom" : "ipad",
138 | "filename" : "Icon-App-76x76@2x.png",
139 | "scale" : "2x"
140 | },
141 | {
142 | "size" : "83.5x83.5",
143 | "idiom" : "ipad",
144 | "filename" : "Icon-App-83.5x83.5@2x.png",
145 | "scale" : "2x"
146 | },
147 | {
148 | "size" : "1024x1024",
149 | "idiom" : "ios-marketing",
150 | "filename" : "ItunesArtwork@2x.png",
151 | "scale" : "1x"
152 | }
153 | ],
154 | "info" : {
155 | "version" : 1,
156 | "author" : "xcode"
157 | }
158 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/Logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "logo1024-universal-341@1x.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "logo1024-universal-341@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "logo1024-universal-341@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/Logo.imageset/logo1024-universal-341@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/Logo.imageset/logo1024-universal-341@1x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/Logo.imageset/logo1024-universal-341@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/Logo.imageset/logo1024-universal-341@2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/Logo.imageset/logo1024-universal-341@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/Logo.imageset/logo1024-universal-341@3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/144_1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/144_1x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/144_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/144_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/144_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/144_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/192_1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/192_1x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/192_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/192_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "filename" : "144_1x.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "filename" : "144_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "filename" : "144_3x.png",
16 | "scale" : "3x"
17 | },
18 | {
19 | "idiom" : "ipad",
20 | "filename" : "192_1x.png",
21 | "scale" : "1x"
22 | },
23 | {
24 | "idiom" : "ipad",
25 | "filename" : "192_2x.png",
26 | "scale" : "2x"
27 | }
28 | ],
29 | "info" : {
30 | "version" : 1,
31 | "author" : "xcode"
32 | }
33 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "filename" : "googlelogo_dark20_color_132x44pt_1x.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "filename" : "googlelogo_dark20_color_132x44pt_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "filename" : "googlelogo_dark20_color_132x44pt_3x.png",
16 | "scale" : "3x"
17 | },
18 | {
19 | "idiom" : "ipad",
20 | "filename" : "googlelogo_dark20_color_184x60pt_1x.png",
21 | "scale" : "1x"
22 | },
23 | {
24 | "idiom" : "ipad",
25 | "filename" : "googlelogo_dark20_color_184x60pt_2x.png",
26 | "scale" : "2x"
27 | }
28 | ],
29 | "info" : {
30 | "version" : 1,
31 | "author" : "xcode"
32 | }
33 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_132x44pt_1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_132x44pt_1x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_132x44pt_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_132x44pt_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_132x44pt_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_132x44pt_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_184x60pt_1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_184x60pt_1x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_184x60pt_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_184x60pt_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_account_circle_36pt.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_account_circle_36pt_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_account_circle_36pt_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/ic_account_circle_36pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/ic_account_circle_36pt.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/ic_account_circle_36pt_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/ic_account_circle_36pt_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/ic_account_circle_36pt_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/ic_account_circle_36pt_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_arrow_back.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_arrow_back_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_arrow_back_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/ic_arrow_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/ic_arrow_back.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/ic_arrow_back_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/ic_arrow_back_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/ic_arrow_back_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/ic_arrow_back_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_close.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_close.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_close_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_close_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_close.imageset/ic_close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_close.imageset/ic_close.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_close.imageset/ic_close_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_close.imageset/ic_close_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_close.imageset/ic_close_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_close.imageset/ic_close_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_comment.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_comment_36pt.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_comment_36pt_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_comment_36pt_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_comment.imageset/ic_comment_36pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_comment.imageset/ic_comment_36pt.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_comment.imageset/ic_comment_36pt_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_comment.imageset/ic_comment_36pt_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_comment.imageset/ic_comment_36pt_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_comment.imageset/ic_comment_36pt_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_comment_white.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_comment_white_36pt.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_comment_white_36pt_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_comment_white_36pt_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_comment_white.imageset/ic_comment_white_36pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_comment_white.imageset/ic_comment_white_36pt.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_comment_white.imageset/ic_comment_white_36pt_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_comment_white.imageset/ic_comment_white_36pt_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_comment_white.imageset/ic_comment_white_36pt_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_comment_white.imageset/ic_comment_white_36pt_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_delete_forever_white.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_delete_forever_white_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_delete_forever_white_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/ic_delete_forever_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/ic_delete_forever_white.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/ic_delete_forever_white_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/ic_delete_forever_white_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/ic_delete_forever_white_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/ic_delete_forever_white_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_favorite.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_favorite_36pt.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_favorite_36pt_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_favorite_36pt_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_favorite.imageset/ic_favorite_36pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_favorite.imageset/ic_favorite_36pt.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_favorite.imageset/ic_favorite_36pt_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_favorite.imageset/ic_favorite_36pt_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_favorite.imageset/ic_favorite_36pt_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_favorite.imageset/ic_favorite_36pt_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_favorite_border_36pt.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_favorite_border_36pt_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_favorite_border_36pt_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/ic_favorite_border_36pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/ic_favorite_border_36pt.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/ic_favorite_border_36pt_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/ic_favorite_border_36pt_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/ic_favorite_border_36pt_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/ic_favorite_border_36pt_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_home.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_home.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_home_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_home_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_home.imageset/ic_home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_home.imageset/ic_home.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_home.imageset/ic_home_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_home.imageset/ic_home_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_home.imageset/ic_home_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_home.imageset/ic_home_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_logout.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_logout_black_1x_ios_24dp.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_logout_black_2x_ios_24dp.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_logout_black_3x_ios_24dp.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_logout.imageset/ic_logout_black_1x_ios_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_logout.imageset/ic_logout_black_1x_ios_24dp.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_logout.imageset/ic_logout_black_2x_ios_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_logout.imageset/ic_logout_black_2x_ios_24dp.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_logout.imageset/ic_logout_black_3x_ios_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_logout.imageset/ic_logout_black_3x_ios_24dp.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_more_vert.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_more_vert.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_more_vert_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_more_vert_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_more_vert.imageset/ic_more_vert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_more_vert.imageset/ic_more_vert.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_more_vert.imageset/ic_more_vert_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_more_vert.imageset/ic_more_vert_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_more_vert.imageset/ic_more_vert_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_more_vert.imageset/ic_more_vert_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_more_vert_white.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_more_vert_white_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_more_vert_white_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/ic_more_vert_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/ic_more_vert_white.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/ic_more_vert_white_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/ic_more_vert_white_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/ic_more_vert_white_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/ic_more_vert_white_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_photo_camera.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_photo_camera_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_photo_camera_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/ic_photo_camera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/ic_photo_camera.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/ic_photo_camera_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/ic_photo_camera_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/ic_photo_camera_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/ic_photo_camera_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_photo_camera_white.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_photo_camera_white_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_photo_camera_white_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/ic_photo_camera_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/ic_photo_camera_white.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/ic_photo_camera_white_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/ic_photo_camera_white_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/ic_photo_camera_white_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/ic_photo_camera_white_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_search.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_search.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_search_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_search_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_search.imageset/ic_search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_search.imageset/ic_search.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_search.imageset/ic_search_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_search.imageset/ic_search_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_search.imageset/ic_search_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_search.imageset/ic_search_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_send.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_send.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_send_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_send_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_send.imageset/ic_send.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_send.imageset/ic_send.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_send.imageset/ic_send_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_send.imageset/ic_send_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_send.imageset/ic_send_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_send.imageset/ic_send_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_trending_up.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ic_trending_up.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "ic_trending_up_2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "ic_trending_up_3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_trending_up.imageset/ic_trending_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_trending_up.imageset/ic_trending_up.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_trending_up.imageset/ic_trending_up_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_trending_up.imageset/ic_trending_up_2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/ic_trending_up.imageset/ic_trending_up_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_trending_up.imageset/ic_trending_up_3x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/image_logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Icon-App-29x29@1x.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "Icon-App-29x29@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "Icon-App-29x29@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/image_logo.imageset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/image_logo.imageset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/image_logo.imageset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/image_logo.imageset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/FriendlyPix/Assets.xcassets/image_logo.imageset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/image_logo.imageset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/FriendlyPix/DateExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | extension Date {
20 |
21 | func timeAgo() -> String {
22 |
23 | let interval = Calendar.current.dateComponents([.year, .day, .hour, .minute, .second], from: self, to: Date())
24 |
25 | if let year = interval.year, year > 0 {
26 | return DateFormatter.localizedString(from: self, dateStyle: .long, timeStyle: .none)
27 | } else if let day = interval.day, day > 6 {
28 | let format = DateFormatter.dateFormat(fromTemplate: "MMMMd", options: 0, locale: NSLocale.current)
29 | let formatter = DateFormatter()
30 | formatter.dateFormat = format
31 | return formatter.string(from: self)
32 | } else if let day = interval.day, day > 0 {
33 | return day == 1 ? "\(day)" + " " + "day ago" :
34 | "\(day)" + " " + "days ago"
35 | } else if let hour = interval.hour, hour > 0 {
36 | return hour == 1 ? "\(hour)" + " " + "hour ago" :
37 | "\(hour)" + " " + "hours ago"
38 | } else if let minute = interval.minute, minute > 0 {
39 | return minute == 1 ? "\(minute)" + " " + "minute ago" :
40 | "\(minute)" + " " + "minutes ago"
41 | } else if let second = interval.second, second > 0 {
42 | return second == 1 ? "\(second)" + " " + "second ago" :
43 | "\(second)" + " " + "seconds ago"
44 | } else {
45 | return "just now"
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/FriendlyPix/FPAccountHeader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import MaterialComponents
18 |
19 | class FPAccountHeader: MDCBaseCell {
20 | @IBOutlet weak var postsLabel: UILabel!
21 | @IBOutlet weak var followingLabel: UILabel!
22 | @IBOutlet weak var followersLabel: UILabel!
23 | @IBOutlet weak var profilePictureImageView: UIImageView!
24 | @IBOutlet weak var followLabel: UILabel!
25 | @IBOutlet weak var followSwitch: UISwitch!
26 | }
27 |
--------------------------------------------------------------------------------
/FriendlyPix/FPAccountViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Firebase
18 | import Lightbox
19 | import MaterialComponents
20 |
21 | class FPAccountViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
22 | var headerView: FPAccountHeader!
23 | var profile: FPUser!
24 | let uid = Auth.auth().currentUser!.uid
25 | let database = Database.database()
26 | let ref = Database.database().reference()
27 | var postIds: [String: Any]?
28 | var postSnapshots = [DataSnapshot]()
29 | var loadingPostCount = 0
30 | var firebaseRefs = [DatabaseReference]()
31 | var insets: UIEdgeInsets!
32 | lazy var appDelegate = UIApplication.shared.delegate as! AppDelegate
33 |
34 | override func viewDidLoad() {
35 | super.viewDidLoad()
36 |
37 |
38 | navigationItem.title = profile.fullname.localizedCapitalized
39 | }
40 |
41 | override func viewDidAppear(_ animated: Bool) {
42 | super.viewDidAppear(animated)
43 | loadData()
44 | }
45 |
46 | override func viewWillDisappear(_ animated: Bool) {
47 | super.viewWillDisappear(animated)
48 | for firebaseRef in firebaseRefs {
49 | firebaseRef.removeAllObservers()
50 | }
51 | firebaseRefs = [DatabaseReference]()
52 | }
53 |
54 | @IBAction func valueChanged(_ sender: Any) {
55 | if profile.uid == uid {
56 | let notificationEnabled = database.reference(withPath: "people/\(uid)/notificationEnabled")
57 | if headerView.followSwitch.isOn {
58 | notificationEnabled.setValue(true)
59 | } else {
60 | notificationEnabled.removeValue()
61 | }
62 | return
63 | }
64 |
65 | toggleFollow(headerView.followSwitch.isOn)
66 | }
67 |
68 |
69 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
70 | if indexPath.section == 0 {
71 | return CGSize(width: collectionView.bounds.size.width, height: 112)
72 | }
73 | let height = ceil(((collectionView.bounds.width) - 14) * 0.325)
74 | return CGSize(width: height, height: height)
75 | }
76 |
77 | func registerToFollowStatusUpdate() {
78 | let followStatusRef = database.reference(withPath: "people/\(uid)/following/\(profile.uid)")
79 | followStatusRef.observe(.value) {
80 | self.headerView.followSwitch.isOn = $0.exists()
81 | }
82 | firebaseRefs.append(followStatusRef)
83 | }
84 |
85 | func registerToNotificationEnabledStatusUpdate() {
86 | let notificationEnabledRef = database.reference(withPath: "people/\(uid)/notificationEnabled")
87 | notificationEnabledRef.observe(.value) {
88 | self.headerView.followSwitch.isOn = $0.exists()
89 | }
90 | firebaseRefs.append(notificationEnabledRef)
91 | }
92 |
93 | func registerForFollowersCount() {
94 | let followersRef = database.reference(withPath: "followers/\(profile.uid)")
95 | followersRef.observe(.value, with: {
96 | self.headerView.followersLabel.text = "\($0.childrenCount) follower\($0.childrenCount != 1 ? "s" : "")"
97 | })
98 | firebaseRefs.append(followersRef)
99 | }
100 |
101 | func registerForFollowingCount() {
102 | let followingRef = database.reference(withPath: "people/\(profile.uid)/following")
103 | followingRef.observe(.value, with: {
104 | self.headerView.followingLabel.text = "\($0.childrenCount) following"
105 | })
106 | firebaseRefs.append(followingRef)
107 | }
108 |
109 | func registerForPostsCount() {
110 | let userPostsRef = database.reference(withPath: "people/\(profile.uid)/posts")
111 | userPostsRef.observe(.value, with: {
112 | self.headerView.postsLabel.text = "\($0.childrenCount) post\($0.childrenCount != 1 ? "s" : "")"
113 | })
114 | }
115 |
116 | func registerForPostsDeletion() {
117 | let userPostsRef = database.reference(withPath: "people/\(profile.uid)/posts")
118 | userPostsRef.observe(.childRemoved, with: { postSnapshot in
119 | var index = 0
120 | for post in self.postSnapshots {
121 | if post.key == postSnapshot.key {
122 | self.postSnapshots.remove(at: index)
123 | self.loadingPostCount -= 1
124 | self.collectionView?.deleteItems(at: [IndexPath(item: index, section: 1)])
125 | return
126 | }
127 | index += 1
128 | }
129 | self.postIds?.removeValue(forKey: postSnapshot.key)
130 | })
131 | }
132 |
133 |
134 | func loadUserPosts() {
135 | database.reference(withPath: "people/\(profile.uid)/posts").observeSingleEvent(of: .value, with: {
136 | if var posts = $0.value as? [String: Any] {
137 | if !self.postSnapshots.isEmpty {
138 | var index = self.postSnapshots.count - 1
139 | self.collectionView?.performBatchUpdates({
140 | for post in self.postSnapshots.reversed() {
141 | if posts.removeValue(forKey: post.key) == nil {
142 | self.postSnapshots.remove(at: index)
143 | self.collectionView?.deleteItems(at: [IndexPath(item: index, section: 1)])
144 | return
145 | }
146 | index -= 1
147 | }
148 | }, completion: nil)
149 | self.postIds = posts
150 | self.loadingPostCount = posts.count
151 | } else {
152 | self.postIds = posts
153 | self.loadFeed()
154 | }
155 | self.registerForPostsDeletion()
156 | }
157 | })
158 | }
159 |
160 | func loadData() {
161 | if profile.uid == uid {
162 | registerToNotificationEnabledStatusUpdate()
163 | } else {
164 | registerToFollowStatusUpdate()
165 | }
166 | registerForFollowersCount()
167 | registerForFollowingCount()
168 | registerForPostsCount()
169 | loadUserPosts()
170 | }
171 |
172 | override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell,
173 | forItemAt indexPath: IndexPath) {
174 | if indexPath.section == 1 && indexPath.item == (loadingPostCount - 3) {
175 | loadFeed()
176 | }
177 | }
178 |
179 | func loadFeed() {
180 | loadingPostCount = postSnapshots.count + 10
181 | self.collectionView?.performBatchUpdates({
182 | for _ in 1...10 {
183 | if let postId = self.postIds?.popFirst()?.key {
184 | database.reference(withPath: "posts/\(postId)").observeSingleEvent(of: .value, with: { postSnapshot in
185 | self.postSnapshots.append(postSnapshot)
186 | self.collectionView?.insertItems(at: [IndexPath(item: self.postSnapshots.count - 1, section: 1)])
187 | })
188 | } else {
189 | break
190 | }
191 | }
192 | }, completion: nil)
193 | }
194 |
195 | override func numberOfSections(in collectionView: UICollectionView) -> Int {
196 | return 2
197 | }
198 |
199 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
200 | if section == 0 {
201 | return 1
202 | }
203 | return postSnapshots.count
204 | }
205 |
206 | override func collectionView(_ collectionView: UICollectionView,
207 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
208 | if indexPath.section == 0 {
209 | let header = collectionView.dequeueReusableCell(withReuseIdentifier: "header", for: indexPath) as! FPAccountHeader
210 | header.inkColor = .clear
211 | headerView = header
212 | if profile.uid == uid {
213 | header.followLabel.text = "Notifications"
214 | header.followSwitch.accessibilityLabel = header.followSwitch.isOn ? "Notifications are on" : "Notifications are off"
215 | header.followSwitch.accessibilityHint = "Double-tap to \(header.followSwitch.isOn ? "disable" : "enable") notifications"
216 | } else {
217 | header.followSwitch.accessibilityHint = "Double-tap to \(header.followSwitch.isOn ? "un" : "")follow"
218 | header.followSwitch.accessibilityLabel = "\(header.followSwitch.isOn ? "" : "not ")following \(profile.fullname)"
219 | }
220 | header.profilePictureImageView.sd_setImage(with: profile.profilePictureURL, completed: nil)
221 | return header
222 | } else {
223 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
224 | let postSnapshot = postSnapshots[indexPath.item]
225 | if let value = postSnapshot.value as? [String: Any], let photoUrl = value["thumb_url"] as? String {
226 | let imageView = UIImageView()
227 | cell.backgroundView = imageView
228 | imageView.sd_setImage(with: URL(string: photoUrl), completed: nil)
229 | imageView.contentMode = .scaleAspectFill
230 | imageView.isAccessibilityElement = true
231 | imageView.accessibilityLabel = "Photo by \(profile.fullname)"
232 | }
233 | return cell
234 | }
235 | }
236 |
237 | lazy var moreAlert: UIAlertController = {
238 | let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
239 | if profile.uid == uid {
240 | alert.addAction(UIAlertAction(title: "Sign out", style: .default , handler:{ (UIAlertAction)in
241 | self.present(self.feedViewController.signOutAlert, animated:true, completion:nil)
242 | }))
243 | alert.addAction(UIAlertAction(title: "Delete account", style: .destructive , handler:{ _ in
244 | self.present(self.deleteAlert, animated:true, completion:nil)
245 | }))
246 | } else {
247 | if !appDelegate.isBlocking(profile.uid) {
248 | alert.addAction(UIAlertAction(title: "Block", style: .destructive , handler:{ _ in
249 | self.present(self.blockAlert, animated:true, completion:nil)
250 | }))
251 | } else {
252 | alert.addAction(UIAlertAction(title: "Unblock", style: .destructive , handler:{ _ in
253 | self.present(self.unblockAlert, animated:true, completion:nil)
254 | }))
255 | }
256 | }
257 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel , handler: nil))
258 | return alert
259 | }()
260 |
261 | @IBAction func didTapMore(_ sender: UIBarButtonItem) {
262 | moreAlert.popoverPresentationController?.barButtonItem = sender
263 | present(moreAlert, animated: true, completion: nil)
264 | }
265 |
266 | func toggleFollow(_ follow: Bool) {
267 | feedViewController.followChanged = true
268 | let myFeed = "feed/\(uid)/"
269 | database.reference(withPath: "people/\(profile.uid)/posts").observeSingleEvent(of: .value, with: { snapshot in
270 | var lastPostID: Any = true
271 | var updateData = [String: Any]()
272 | if let posts = snapshot.value as? [String: Any] {
273 | // Add/remove followed user's posts to the home feed.
274 | for postId in posts.keys {
275 | updateData[myFeed + postId] = follow ? true : NSNull()
276 | lastPostID = postId
277 | }
278 |
279 | // Add/remove followed user to the 'following' list.
280 | updateData["people/\(self.uid)/following/\(self.profile.uid)"] = follow ? lastPostID : NSNull()
281 |
282 | // Add/remove signed-in user to the list of followers.
283 | updateData["followers/\(self.profile.uid)/\(self.uid)"] = follow ? true : NSNull()
284 | self.ref.updateChildValues(updateData) { error, _ in
285 | if let error = error {
286 | print(error.localizedDescription)
287 | }
288 | }
289 | }
290 | })
291 | }
292 |
293 | func collectionView(_ collectionView: UICollectionView, cellHeightAt indexPath: IndexPath) -> CGFloat {
294 | if indexPath.section == 0 {
295 | return 112
296 | }
297 | return ceil(((self.collectionView?.bounds.width)! - 14) * 0.325)
298 | }
299 |
300 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
301 | if indexPath.section != 0 {
302 | performSegue(withIdentifier: "detail", sender: postSnapshots[indexPath.item])
303 | }
304 | }
305 |
306 | func backButtonAction(_ sender: Any) {
307 | navigationController?.popViewController(animated: true)
308 | }
309 |
310 | lazy var errorAlert: MDCAlertController = {
311 | let alertController = MDCAlertController(title: "Deletion requires recent authentication",
312 | message: "Log in again before retrying.")
313 | let okAction = MDCAlertAction(title:"OK") { _ in self.feedViewController.signOut() }
314 | alertController.addAction(okAction)
315 | return alertController
316 | }()
317 |
318 | lazy var deleteAlert: MDCAlertController = {
319 | let alertController = MDCAlertController.init(title: "Delete Account?", message: nil)
320 | let cancelAction = MDCAlertAction(title:"Cancel", handler: nil)
321 | let deleteAction = MDCAlertAction(title:"Delete") { _ in
322 | Auth.auth().currentUser?.delete(completion: { error in
323 | if error != nil {
324 | self.present(self.errorAlert, animated:true, completion:nil)
325 | return
326 | }
327 | self.feedViewController.signOut()
328 | })
329 | }
330 | alertController.addAction(deleteAction)
331 | alertController.addAction(cancelAction)
332 | return alertController
333 | }()
334 |
335 | lazy var blockAlert: MDCAlertController = {
336 | let alertController = MDCAlertController.init(title: "Block Account?", message: nil)
337 | let cancelAction = MDCAlertAction(title:"Cancel", handler: nil)
338 | let blockAction = MDCAlertAction(title:"Block") { _ in
339 | if self.headerView.followSwitch.isOn {
340 | self.toggleFollow(false)
341 | }
342 | let updateData = ["blocked/\(self.profile.uid)/\(self.uid)": true,
343 | "blocking/\(self.uid)/\(self.profile.uid)" : true]
344 | self.ref.updateChildValues(updateData) { error, _ in
345 | if let error = error {
346 | print(error.localizedDescription)
347 | }
348 | }
349 | }
350 | alertController.addAction(blockAction)
351 | alertController.addAction(cancelAction)
352 | return alertController
353 | }()
354 |
355 | lazy var unblockAlert: MDCAlertController = {
356 | let alertController = MDCAlertController.init(title: "Unblock Account?", message: nil)
357 | let cancelAction = MDCAlertAction(title:"Cancel", handler: nil)
358 | let unblockAction = MDCAlertAction(title:"Unblock") { _ in
359 |
360 | let updateData = ["blocked/\(self.profile.uid)/\(self.uid)": NSNull(),
361 | "blocking/\(self.uid)/\(self.profile.uid)" : NSNull()]
362 | self.ref.updateChildValues(updateData) { error, _ in
363 | if let error = error {
364 | print(error.localizedDescription)
365 | }
366 | }
367 | }
368 | alertController.addAction(unblockAction)
369 | alertController.addAction(cancelAction)
370 | return alertController
371 | }()
372 |
373 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
374 | if segue.identifier == "detail" {
375 | if let detailViewController = segue.destination as? FPPostDetailViewController,
376 | let sender = sender as? DataSnapshot {
377 | detailViewController.postSnapshot = sender
378 | }
379 | }
380 | }
381 | }
382 |
--------------------------------------------------------------------------------
/FriendlyPix/FPAuthPickerViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import FirebaseUI
18 | import MaterialComponents.MDCTypography
19 |
20 | class FPAuthPickerViewController: FUIAuthPickerViewController {
21 | @IBOutlet var readonlyWarningLabel: UILabel!
22 | let attributes = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]
23 | let attributes2 = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)]
24 | var agreed = false
25 |
26 | lazy var disclaimer: MDCAlertController = {
27 | let alertController = MDCAlertController(title: nil, message: "I understand FriendlyPix is an application aimed at showcasing the Firebase platform capabilities, and should not be used with private or sensitive information. All FriendlyPix data and inactive accounts are regularly removed. I agree to the Terms of Service and Privacy Policy.")
28 |
29 | let acceptAction = MDCAlertAction(title: "I agree", emphasis: .high) { action in
30 | self.agreed = true
31 | }
32 | alertController.addAction(acceptAction)
33 | let termsAction = MDCAlertAction(title: "Terms") { action in
34 | UIApplication.shared.open(URL(string: "https://friendly-pix.com/terms")!,
35 | options: [:], completionHandler: { completion in
36 | self.present(alertController, animated: true, completion: nil)
37 | })
38 | }
39 | alertController.addAction(termsAction)
40 | let policyAction = MDCAlertAction(title: "Privacy") { action in
41 | UIApplication.shared.open(URL(string: "https://www.google.com/policies/privacy")!,
42 | options: [:], completionHandler: { completion in
43 | self.present(alertController, animated: true, completion: nil)
44 | })
45 | }
46 | alertController.addAction(policyAction)
47 | let colorScheme = MDCSemanticColorScheme()
48 | MDCAlertColorThemer.applySemanticColorScheme(colorScheme, to: alertController)
49 | return alertController
50 | }()
51 |
52 | override func viewDidAppear(_ animated: Bool) {
53 | super.viewDidAppear(animated)
54 | if (AppDelegate.euroZone) {
55 | readonlyWarningLabel.isHidden = false
56 | }
57 | if !agreed {
58 | self.present(disclaimer, animated: true, completion: nil)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/FriendlyPix/FPAuthPickerViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Amaranth-Bold
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/FriendlyPix/FPCardCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import MaterialComponents
18 | import SDWebImage
19 | import Firebase
20 |
21 | protocol FPCardCollectionViewCellDelegate: class {
22 | func showProfile(_ author: FPUser)
23 | func showTaggedPhotos(_ hashtag: String)
24 | func showLightbox(_ index: Int)
25 | func viewComments(_ post: FPPost)
26 | func toogleLike(_ post: FPPost, label: UILabel)
27 | func optionPost(_ post: FPPost, _ button: UIButton, completion: (() -> Swift.Void)? )
28 | }
29 |
30 | class FPCardCollectionViewCell: MDCCardCollectionCell {
31 | @IBOutlet weak private var authorImageView: UIImageView!
32 | @IBOutlet weak private var authorLabel: UILabel!
33 | @IBOutlet weak private var dateLabel: UILabel!
34 | @IBOutlet weak private var postImageView: UIImageView!
35 | @IBOutlet weak private var titleLabel: UILabel!
36 | @IBOutlet weak private var likesLabel: UILabel!
37 | @IBOutlet weak private var likeButton: UIButton!
38 | @IBOutlet weak private var comment1Label: UILabel!
39 | @IBOutlet weak private var comment2Label: UILabel!
40 | @IBOutlet weak private var viewAllCommentsLabel: UIButton!
41 | var commentLabels: [UILabel]?
42 | let attributes = [NSAttributedString.Key.font: UIFont.mdc_preferredFont(forMaterialTextStyle: .body2)]
43 |
44 | var post: FPPost!
45 | weak var delegate: FPCardCollectionViewCellDelegate?
46 | var labelConstraints: [NSLayoutConstraint]!
47 | public var imageConstraint: NSLayoutConstraint?
48 |
49 | override func awakeFromNib() {
50 | super.awakeFromNib()
51 |
52 | authorImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(profileTapped)))
53 | authorLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(profileTapped)))
54 | postImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imageTapped)))
55 | authorImageView.isAccessibilityElement = true
56 | authorImageView.accessibilityHint = "Double-tap to open profile."
57 |
58 | commentLabels = [comment1Label, comment2Label]
59 |
60 | comment1Label.addGestureRecognizer(UITapGestureRecognizer(target: self,
61 | action: #selector(handleTapOnComment(recognizer:))))
62 | comment2Label.addGestureRecognizer(UITapGestureRecognizer(target: self,
63 | action: #selector(handleTapOnComment(recognizer:))))
64 | titleLabel.preferredMaxLayoutWidth = self.bounds.width - 16
65 | comment1Label.preferredMaxLayoutWidth = titleLabel.preferredMaxLayoutWidth
66 | comment2Label.preferredMaxLayoutWidth = titleLabel.preferredMaxLayoutWidth
67 | }
68 |
69 | private func convertCacheTypeToString(_ cacheType: SDImageCacheType) -> String {
70 | switch cacheType {
71 | case .none:
72 | return "none"
73 | case .disk:
74 | return "disk"
75 | case .memory:
76 | return "memory"
77 | case .all:
78 | return "all"
79 | }
80 | }
81 |
82 | func populateContent(post: FPPost, index: Int, isDryRun: Bool) {
83 | if Auth.auth().currentUser!.isAnonymous {
84 | likeButton.isEnabled = false
85 | }
86 | self.post = post
87 | let postAuthor = post.author
88 | if !isDryRun, let profilePictureURL = postAuthor.profilePictureURL {
89 | UIImage.circleImage(with: profilePictureURL, to: authorImageView)
90 | authorImageView.accessibilityLabel = postAuthor.fullname
91 | }
92 | authorLabel.text = postAuthor.fullname
93 | dateLabel.text = post.postDate.timeAgo()
94 | postImageView.tag = index
95 | if !isDryRun {
96 | let trace = Performance.startTrace(name: "post_load")
97 | postImageView?.sd_setImage(with: post.thumbURL, completed: { image, error, cacheType, url in
98 | trace?.incrementMetric(self.convertCacheTypeToString(cacheType), by: 1)
99 | trace?.stop()
100 | })
101 | postImageView.accessibilityLabel = "Photo by \(postAuthor.fullname)"
102 | }
103 |
104 | let title = NSMutableAttributedString(string: postAuthor.fullname + " ", attributes: attributes)
105 | let attrString = NSMutableAttributedString(string: post.text)
106 | let regex = try? NSRegularExpression(pattern: "(#[a-zA-Z0-9_\\p{Arabic}\\p{N}]*)", options: [])
107 | if let matches = regex?.matches(in: post.text, options:[], range:NSMakeRange(0, post.text.count)) {
108 | for match in matches {
109 | attrString.addAttribute(NSAttributedString.Key.link, value: (post.text as NSString).substring(with: match.range), range: match.range)
110 | attrString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.blue , range: match.range)
111 | }
112 | }
113 | title.append(attrString)
114 | title.addAttribute(.paragraphStyle, value: MDCSelfSizingStereoCell.paragraphStyle, range: NSMakeRange(0, title.length))
115 | titleLabel.attributedText = title
116 | titleLabel.accessibilityLabel = "\(post.text), posted by \(postAuthor.fullname)"
117 |
118 | titleLabel.addGestureRecognizer(UITapGestureRecognizer(target: self,
119 | action: #selector(handleTapOnProfileLabel(recognizer:))))
120 | likesLabel.text = post.likeCount == 1 ? "1 like" : "\(post.likeCount) likes"
121 | likesLabel.font = UIFont.mdc_preferredFont(forMaterialTextStyle: .body2)
122 | if post.isLiked {
123 | likeButton.setImage(#imageLiteral(resourceName: "ic_favorite"), for: .normal)
124 | likeButton.accessibilityLabel = "you liked this post"
125 | likeButton.accessibilityHint = "double-tap to unlike"
126 | } else {
127 | likeButton.setImage(#imageLiteral(resourceName: "ic_favorite_border"), for: .normal)
128 | likeButton.accessibilityLabel = "you haven't liked this post"
129 | likeButton.accessibilityHint = "double-tap to like"
130 | }
131 |
132 | if labelConstraints != nil {
133 | NSLayoutConstraint.deactivate(labelConstraints)
134 | labelConstraints = nil
135 | }
136 |
137 | let betweenConstant: CGFloat = 2
138 | let bottomConstant: CGFloat = 12
139 | let commentCount = post.comments.count
140 | switch commentCount {
141 | case 0:
142 | labelConstraints = [contentView.bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor,
143 | constant: bottomConstant)]
144 | viewAllCommentsLabel.isHidden = true
145 | comment1Label.isHidden = true
146 | comment1Label.text = nil
147 | comment2Label.isHidden = true
148 | comment2Label.text = nil
149 | case 1:
150 | labelConstraints = [comment1Label.topAnchor.constraint(equalTo: titleLabel.bottomAnchor,
151 | constant: betweenConstant),
152 | contentView.bottomAnchor.constraint(equalTo: comment1Label.bottomAnchor,
153 | constant: bottomConstant)]
154 | viewAllCommentsLabel.isHidden = true
155 | attributeComment(index: 0)
156 | comment2Label.isHidden = true
157 | comment2Label.text = nil
158 | case 2:
159 | labelConstraints = [comment1Label.topAnchor.constraint(equalTo: titleLabel.bottomAnchor,
160 | constant: betweenConstant),
161 | comment2Label.topAnchor.constraint(equalTo: comment1Label.bottomAnchor,
162 | constant: betweenConstant),
163 | contentView.bottomAnchor.constraint(equalTo: comment2Label.bottomAnchor,
164 | constant: bottomConstant)]
165 | viewAllCommentsLabel.isHidden = true
166 | attributeComment(index: 0)
167 | attributeComment(index: 1)
168 | default:
169 | labelConstraints = [viewAllCommentsLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor,
170 | constant: betweenConstant),
171 | comment1Label.topAnchor.constraint(equalTo: viewAllCommentsLabel.bottomAnchor,
172 | constant: betweenConstant),
173 | comment2Label.topAnchor.constraint(equalTo: comment1Label.bottomAnchor,
174 | constant: betweenConstant),
175 | contentView.bottomAnchor.constraint(equalTo: comment2Label.bottomAnchor,
176 | constant: bottomConstant)]
177 | viewAllCommentsLabel.isHidden = false
178 | viewAllCommentsLabel.setTitle("View all \(commentCount) comments", for: .normal)
179 | attributeComment(index: 0)
180 | attributeComment(index: 1)
181 | }
182 | NSLayoutConstraint.activate(labelConstraints)
183 | }
184 |
185 | private func attributeComment(index: Int) {
186 | if let commentLabel = commentLabels?[index] {
187 | let comment = post.comments[index]
188 | commentLabel.isHidden = false
189 | commentLabel.accessibilityLabel = "\(comment.from.fullname) said, \(comment.text)"
190 | let text = NSMutableAttributedString(string: comment.from.fullname, attributes: attributes)
191 | text.append(NSAttributedString(string: " " + comment.text))
192 | text.addAttribute(.paragraphStyle, value: MDCSelfSizingStereoCell.paragraphStyle, range: NSMakeRange(0, text.length))
193 | commentLabel.attributedText = text
194 | }
195 | }
196 |
197 | override func updateConstraints() {
198 | super.updateConstraints()
199 |
200 | let constant = ceil((self.bounds.width - 2) * 0.65)
201 | if imageConstraint == nil {
202 | imageConstraint = postImageView.heightAnchor.constraint(equalToConstant: constant)
203 |
204 | imageConstraint?.isActive = true
205 | }
206 | imageConstraint?.constant = constant
207 | }
208 |
209 | @IBAction func toggledLike() {
210 | delegate?.toogleLike(post, label: likesLabel)
211 | }
212 |
213 | @IBAction func tappedOption(_ sender: UIButton) {
214 | delegate?.optionPost(post, sender, completion: nil)
215 | }
216 |
217 | override func prepareForReuse() {
218 | super.prepareForReuse()
219 | NSLayoutConstraint.deactivate(labelConstraints)
220 | labelConstraints = nil
221 | }
222 |
223 | @objc func profileTapped() {
224 | delegate?.showProfile(post.author)
225 | }
226 |
227 | @objc func imageTapped() {
228 | delegate?.showLightbox(postImageView.tag)
229 | }
230 |
231 | @objc func handleTapOnProfileLabel(recognizer: UITapGestureRecognizer) {
232 | let touchIndex = recognizer.touchIndexInLabel(label: titleLabel)
233 | if touchIndex < post.author.fullname.count {
234 | profileTapped()
235 | } else if let tag = titleLabel.attributedText?.attribute(NSAttributedString.Key.link, at: touchIndex, effectiveRange: nil) as? String {
236 | delegate?.showTaggedPhotos(String(tag.dropFirst()))
237 | }
238 | }
239 |
240 | @objc func handleTapOnComment(recognizer: UITapGestureRecognizer) {
241 | if let index = recognizer.view?.tag {
242 | let from = post.comments[index].from
243 | if recognizer.didTapAttributedTextInLabel(label: commentLabels![index],
244 | inRange: NSRange(location: 0,
245 | length: from.fullname.count)) {
246 | delegate?.showProfile(from)
247 | }
248 | }
249 | }
250 |
251 | @IBAction func viewAllComments(_ sender: Any) {
252 | delegate?.viewComments(post)
253 | }
254 |
255 | var layerClass: AnyClass {
256 | return MDCShadowLayer.self
257 | }
258 | }
259 |
260 | extension UITapGestureRecognizer {
261 |
262 | func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
263 | return NSLocationInRange(touchIndexInLabel(label: label), targetRange)
264 | }
265 |
266 | func touchIndexInLabel(label: UILabel) -> Int {
267 | // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
268 | let layoutManager = NSLayoutManager()
269 | let textContainer = NSTextContainer(size: CGSize.zero)
270 | let textStorage = NSTextStorage(attributedString: label.attributedText!)
271 |
272 | // Configure layoutManager and textStorage
273 | layoutManager.addTextContainer(textContainer)
274 | textStorage.addLayoutManager(layoutManager)
275 |
276 | // Configure textContainer
277 | textContainer.lineFragmentPadding = 0.0
278 | textContainer.lineBreakMode = label.lineBreakMode
279 | textContainer.maximumNumberOfLines = label.numberOfLines
280 | let labelSize = label.bounds.size
281 | textContainer.size = labelSize
282 |
283 | // Find the tapped character location and compare it to the specified range
284 | let locationOfTouchInLabel = self.location(in: label)
285 | let textBoundingBox = layoutManager.usedRect(for: textContainer)
286 | let textContainerOffset =
287 | CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
288 | y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
289 | let locationOfTouchInTextContainer =
290 | CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x,
291 | y: locationOfTouchInLabel.y - textContainerOffset.y)
292 | let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer,
293 | in: textContainer,
294 | fractionOfDistanceBetweenInsertionPoints: nil)
295 | return indexOfCharacter
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/FriendlyPix/FPCollectionViewTextCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import MaterialComponents
18 |
19 | class FPCollectionViewTextCell: MDCCollectionViewTextCell {
20 | override func prepareForReuse() {
21 | super.prepareForReuse()
22 | textLabel?.numberOfLines = 0
23 | detailTextLabel?.numberOfLines = 0
24 | setNeedsLayout()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/FriendlyPix/FPComment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Firebase
18 |
19 | class FPComment {
20 | var commentID: String
21 | var text: String
22 | var postDate: Date
23 | var from: FPUser
24 |
25 | init(snapshot: DataSnapshot) {
26 | self.commentID = snapshot.key
27 | let value = snapshot.value as! [String: Any]
28 | self.text = value["text"] as? String ?? ""
29 | let timestamp = value["timestamp"] as! Double
30 | self.postDate = Date(timeIntervalSince1970: timestamp / 1_000.0)
31 | let author = value["author"] as! [String: String]
32 | self.from = FPUser(dictionary: author)
33 | }
34 | }
35 |
36 | extension FPComment: Equatable {
37 | static func ==(lhs: FPComment, rhs: FPComment) -> Bool {
38 | return lhs.commentID == rhs.commentID && lhs.postDate == rhs.postDate
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/FriendlyPix/FPCommentCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Firebase
18 | import MaterialComponents
19 |
20 | extension MDCSelfSizingStereoCell {
21 |
22 | static let attributes = [NSAttributedString.Key.font: UIFont.mdc_preferredFont(forMaterialTextStyle: .body2)]
23 | static let attributes2 = [NSAttributedString.Key.font: UIFont.mdc_preferredFont(forMaterialTextStyle: .body1)]
24 |
25 | func populateContent(from: FPUser, text: String, date: Date, index: Int) {
26 | let attrText = NSMutableAttributedString(string: from.fullname , attributes: MDCSelfSizingStereoCell.attributes)
27 | attrText.append(NSAttributedString(string: " " + text, attributes: MDCSelfSizingStereoCell.attributes2))
28 | attrText.addAttribute(.paragraphStyle, value: MDCSelfSizingStereoCell.paragraphStyle, range: NSMakeRange(0, attrText.length))
29 | titleLabel.attributedText = attrText
30 | titleLabel.accessibilityLabel = "\(from.fullname) said, \(text)"
31 | if let profilePictureURL = from.profilePictureURL {
32 | UIImage.circleImage(with: profilePictureURL, to: leadingImageView)
33 | leadingImageView.accessibilityLabel = from.fullname
34 | leadingImageView.accessibilityHint = "Double-tap to open profile."
35 | }
36 | leadingImageView.tag = index
37 | titleLabel.tag = index
38 | detailLabel.text = date.timeAgo()
39 | }
40 |
41 |
42 | static let paragraphStyle = { () -> NSMutableParagraphStyle in
43 | let style = NSMutableParagraphStyle()
44 | style.lineSpacing = 2
45 | return style
46 | }()
47 | }
48 |
--------------------------------------------------------------------------------
/FriendlyPix/FPHashTagViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2018 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Firebase
18 | import Lightbox
19 | import MaterialComponents
20 |
21 | class FPHashTagViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
22 | var hashtag = ""
23 | let uid = Auth.auth().currentUser!.uid
24 | let database = Database.database()
25 | let ref = Database.database().reference()
26 | var postIds: [String: Any]?
27 | var postSnapshots = [DataSnapshot]()
28 | var loadingPostCount = 0
29 | var firebaseRefs = [DatabaseReference]()
30 | lazy var appDelegate = UIApplication.shared.delegate as! AppDelegate
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 | navigationItem.title = "#\(hashtag)"
35 | }
36 |
37 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
38 | return postSnapshots.count
39 | }
40 |
41 | override func viewDidAppear(_ animated: Bool) {
42 | super.viewDidAppear(animated)
43 | loadData()
44 | }
45 |
46 | override func viewWillDisappear(_ animated: Bool) {
47 | super.viewWillDisappear(animated)
48 | for firebaseRef in firebaseRefs {
49 | firebaseRef.removeAllObservers()
50 | }
51 | firebaseRefs = [DatabaseReference]()
52 | }
53 |
54 | func registerForPostsDeletion() {
55 | let userPostsRef = database.reference(withPath: "hashtags/\(hashtag)")
56 | userPostsRef.observe(.childRemoved, with: { postSnapshot in
57 | var index = 0
58 | for post in self.postSnapshots {
59 | if post.key == postSnapshot.key {
60 | self.postSnapshots.remove(at: index)
61 | self.loadingPostCount -= 1
62 | self.collectionView?.deleteItems(at: [IndexPath(item: index, section: 0)])
63 | return
64 | }
65 | index += 1
66 | }
67 | self.postIds?.removeValue(forKey: postSnapshot.key)
68 | })
69 | }
70 |
71 |
72 | func loadUserPosts() {
73 | database.reference(withPath: "hashtags/\(hashtag.lowercased())").observeSingleEvent(of: .value, with: {
74 | if var posts = $0.value as? [String: Any] {
75 | if !self.postSnapshots.isEmpty {
76 | var index = self.postSnapshots.count - 1
77 | self.collectionView?.performBatchUpdates({
78 | for post in self.postSnapshots.reversed() {
79 | if posts.removeValue(forKey: post.key) == nil {
80 | self.postSnapshots.remove(at: index)
81 | self.collectionView?.deleteItems(at: [IndexPath(item: index, section: 0)])
82 | return
83 | }
84 | index -= 1
85 | }
86 | }, completion: nil)
87 | self.postIds = posts
88 | self.loadingPostCount = posts.count
89 | } else {
90 | self.postIds = posts
91 | self.loadFeed()
92 | }
93 | self.registerForPostsDeletion()
94 | }
95 | })
96 | }
97 |
98 | func loadData() {
99 | loadUserPosts()
100 | }
101 |
102 | override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell,
103 | forItemAt indexPath: IndexPath) {
104 | if indexPath.item == (loadingPostCount - 3) {
105 | loadFeed()
106 | }
107 | }
108 |
109 | func loadFeed() {
110 | loadingPostCount = postSnapshots.count + 12
111 | self.collectionView?.performBatchUpdates({
112 | for _ in 1...12 {
113 | if let postId = self.postIds?.popFirst()?.key {
114 | database.reference(withPath: "posts/\(postId)").observeSingleEvent(of: .value, with: { postSnapshot in
115 | self.postSnapshots.append(postSnapshot)
116 | self.collectionView?.insertItems(at: [IndexPath(item: self.postSnapshots.count - 1, section: 0)])
117 | })
118 | } else {
119 | break
120 | }
121 | }
122 | }, completion: nil)
123 | }
124 |
125 | override func collectionView(_ collectionView: UICollectionView,
126 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
127 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
128 | let postSnapshot = postSnapshots[indexPath.item]
129 | if let value = postSnapshot.value as? [String: Any], let photoUrl = value["thumb_url"] as? String {
130 | let imageView = UIImageView()
131 | cell.backgroundView = imageView
132 | imageView.sd_setImage(with: URL(string: photoUrl), completed: nil)
133 | imageView.contentMode = .scaleAspectFill
134 | imageView.isAccessibilityElement = true
135 | imageView.accessibilityLabel = "Photo with hashtag \(hashtag)"
136 | }
137 | return cell
138 | }
139 |
140 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
141 | let height = ceil(((self.collectionView.bounds.width) - 14) * 0.325)
142 | return CGSize(width: height, height: height)
143 | }
144 |
145 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
146 | performSegue(withIdentifier: "detail", sender: postSnapshots[indexPath.item])
147 | }
148 |
149 | func backButtonAction(_ sender: Any) {
150 | navigationController?.popViewController(animated: true)
151 | }
152 |
153 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
154 | if segue.identifier == "detail" {
155 | if let detailViewController = segue.destination as? FPPostDetailViewController,
156 | let sender = sender as? DataSnapshot {
157 | detailViewController.postSnapshot = sender
158 | }
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/FriendlyPix/FPPost.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2018 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Firebase
18 |
19 | class FPPost {
20 | var postID: String
21 | var postDate: Date
22 | var thumbURL: URL
23 | var fullURL: URL
24 | var author: FPUser
25 | var text: String
26 | var comments: [FPComment]
27 | var isLiked = false
28 | var mine = false
29 | var likeCount = 0
30 |
31 | convenience init(snapshot: DataSnapshot, andComments comments: [FPComment], andLikes likes: [String: Any]?) {
32 | self.init(id: snapshot.key, value: snapshot.value as! [String : Any], andComments: comments, andLikes: likes)
33 | }
34 |
35 | init(id: String, value: [String: Any], andComments comments: [FPComment], andLikes likes: [String: Any]?) {
36 | self.postID = id
37 | self.text = value["text"] as! String
38 | let timestamp = value["timestamp"] as! Double
39 | self.postDate = Date(timeIntervalSince1970: (timestamp / 1_000.0))
40 | let author = value["author"] as! [String: String]
41 | self.author = FPUser(dictionary: author)
42 | self.thumbURL = URL(string: value["thumb_url"] as! String)!
43 | self.fullURL = URL(string: value["full_url"] as! String)!
44 | self.comments = comments
45 | if let likes = likes {
46 | likeCount = likes.count
47 | if let uid = Auth.auth().currentUser?.uid {
48 | isLiked = (likes.index(forKey: uid) != nil)
49 | }
50 | }
51 | self.mine = self.author == Auth.auth().currentUser!
52 | }
53 | }
54 |
55 | extension FPPost: Equatable {
56 | static func ==(lhs: FPPost, rhs: FPPost) -> Bool {
57 | return lhs.postID == rhs.postID
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/FriendlyPix/FPPostDetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Firebase
18 |
19 | class FPPostDetailViewController: FPFeedViewController {
20 | var postSnapshot: DataSnapshot!
21 |
22 | override func loadData() {
23 | if let post = posts.first {
24 | postsRef.child(post.postID).observeSingleEvent(of: .value, with: {
25 | if $0.exists() && !self.appDelegate.isBlocked($0) {
26 | self.updatePost(post, postSnapshot: $0)
27 | self.listenPost(post)
28 | } else {
29 | self.navigationController?.popViewController(animated: true)
30 | }
31 | })
32 | } else {
33 | loadPost(postSnapshot)
34 | }
35 | }
36 |
37 | override func optionPost(_ post: FPPost, _ button: UIButton, completion: (() -> Swift.Void)? = nil) {
38 | super.optionPost(post, button, completion: { self.navigationController?.popViewController(animated: true) })
39 | }
40 |
41 | override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell,
42 | forItemAt indexPath: IndexPath) {
43 | }
44 |
45 | override func awakeFromNib() {
46 | }
47 |
48 | override func showProfile(_ profile: FPUser) {
49 | feedViewController.showProfile(profile)
50 | }
51 |
52 | override func viewComments(_ post: FPPost) {
53 | feedViewController.viewComments(post)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/FriendlyPix/FPSearchViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Firebase
18 | import MaterialComponents
19 |
20 | class FPSearchViewController: UICollectionViewController, UISearchBarDelegate, UISearchControllerDelegate {
21 |
22 | let searchController = UISearchController(searchResultsController: nil)
23 | let peopleRef = Database.database().reference(withPath: "people")
24 | let hashtagsRef = Database.database().reference(withPath: "hashtags")
25 | lazy var appDelegate = UIApplication.shared.delegate as! AppDelegate
26 | lazy var uid = Auth.auth().currentUser!.uid
27 | var people = [FPUser]()
28 | var hashtags = [String]()
29 | let emptyLabel: UILabel = {
30 | let messageLabel = UILabel()
31 | messageLabel.text = "No people or hashtag found."
32 | messageLabel.textColor = UIColor.black
33 | messageLabel.numberOfLines = 0
34 | messageLabel.textAlignment = .center
35 | messageLabel.font = UIFont.preferredFont(forTextStyle: .title3)
36 | messageLabel.sizeToFit()
37 | return messageLabel
38 | }()
39 | // We keep track of the pending work item as a property
40 | private var pendingRequestWorkItem: DispatchWorkItem?
41 |
42 | override func viewDidLoad() {
43 | super.viewDidLoad()
44 | collectionView.register(MDCSelfSizingStereoCell.self, forCellWithReuseIdentifier: "cell")
45 | // Setup the Search Controller
46 | searchController.searchResultsUpdater = self
47 | searchController.searchBar.delegate = self
48 | searchController.delegate = self
49 | searchController.obscuresBackgroundDuringPresentation = false
50 | searchController.hidesNavigationBarDuringPresentation = false
51 | searchController.searchBar.placeholder = "Search"
52 |
53 | UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).leftViewMode = .never
54 | searchController.searchBar.setImage(#imageLiteral(resourceName: "ic_close"), for: .clear, state: .normal)
55 | //searchController.searchBar.setImage(#imageLiteral(resourceName: "ic_arrow_back"), for: .search, state: .normal)
56 | UIImageView.appearance(whenContainedInInstancesOf: [UISearchBar.self]).bounds = CGRect(x: 0, y: 0, width: 24, height: 24)
57 |
58 | guard let collectionViewLayout = self.collectionViewLayout as? UICollectionViewFlowLayout else { return }
59 |
60 | collectionViewLayout.estimatedItemSize = CGSize(width: collectionView.bounds.size.width,
61 | height: 75)
62 |
63 | let x = UIButton.init()
64 | x.setImage(#imageLiteral(resourceName: "ic_arrow_back"), for: .normal)
65 | x.addTarget(self, action: #selector(back), for: .touchUpInside)
66 |
67 | UIButton.appearance(whenContainedInInstancesOf: [UINavigationBar.self]).translatesAutoresizingMaskIntoConstraints = false
68 | UIButton.appearance(whenContainedInInstancesOf: [UINavigationBar.self]).contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -4)
69 |
70 | navigationItem.leftBarButtonItem = UIBarButtonItem(customView: x)
71 |
72 | navigationItem.titleView = searchController.searchBar
73 | navigationItem.hidesBackButton = true
74 | definesPresentationContext = true
75 | searchController.searchBar.showsCancelButton = false
76 |
77 | UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).tintColor = UIColor.init(red: 0, green: 137/255, blue: 249/255, alpha: 1)
78 | }
79 |
80 | @objc func back() {
81 | navigationController?.popViewController(animated: true)
82 | }
83 |
84 | func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
85 | searchController.searchBar.showsCancelButton = false
86 | self.searchController.searchBar.becomeFirstResponder()
87 | }
88 |
89 | func didPresentSearchController(_ searchController: UISearchController) {
90 | searchController.searchBar.showsCancelButton = false
91 | self.searchController.searchBar.becomeFirstResponder()
92 | }
93 |
94 | override func viewDidAppear(_ animated: Bool) {
95 | super.viewDidAppear(animated)
96 | DispatchQueue.global(qos: .default).async(execute: {() -> Void in
97 | DispatchQueue.main.async(execute: {() -> Void in
98 | self.searchController.searchBar.becomeFirstResponder()
99 | })
100 | })
101 | }
102 |
103 | override func viewWillAppear(_ animated: Bool) {
104 | super.viewWillAppear(animated)
105 | navigationController?.navigationBar.barTintColor = .white
106 | navigationController?.navigationBar.tintColor = .gray
107 | }
108 |
109 | override func viewWillDisappear(_ animated: Bool) {
110 | super.viewWillDisappear(animated)
111 | navigationController?.navigationBar.barTintColor = UIColor(red: 12, green: 34, blue: 56, alpha: 1)
112 | navigationController?.navigationBar.tintColor = .white
113 | }
114 |
115 | func filterContentForSearchText(_ searchText: String, scope: String = "All") {
116 | if searchText.isEmpty {
117 | return
118 | }
119 | let searchString = searchText.lowercased()
120 | // Cancel the currently pending item
121 | pendingRequestWorkItem?.cancel()
122 |
123 | // Wrap our request in a work item
124 | let requestWorkItem = DispatchWorkItem { [weak self] in
125 | self?.people = [FPUser]()
126 | self?.hashtags = [String]()
127 | self?.collectionView?.reloadData()
128 | self?.collectionView?.performBatchUpdates({
129 | self?.search(searchString, at: "full_name")
130 | self?.search(searchString, at: "reversed_full_name")
131 | self?.searchHashtags(searchString)
132 | }, completion: nil)
133 | }
134 |
135 | // Save the new work item and execute it after 250 ms
136 | pendingRequestWorkItem = requestWorkItem
137 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250),
138 | execute: requestWorkItem)
139 | }
140 |
141 | private func search(_ searchString: String, at index: String) {
142 | peopleRef.queryOrdered(byChild: "_search_index/\(index)").queryStarting(atValue: searchString)
143 | .queryLimited(toFirst: 10).observeSingleEvent(of: .value, with: { snapshot in
144 | let enumerator = snapshot.children
145 | while let person = enumerator.nextObject() as? DataSnapshot {
146 | if !self.appDelegate.isBlocked(by: person.key), let value = person.value as? [String: Any], let searchIndex = value["_search_index"] as? [String: Any],
147 | let fullName = searchIndex[index] as? String, fullName.hasPrefix(searchString) {
148 | self.people.append(FPUser(snapshot: person))
149 | self.collectionView?.insertItems(at: [IndexPath(item: self.people.count - 1, section: 0)])
150 | }
151 | }
152 | })
153 | }
154 |
155 | private func searchHashtags(_ searchString: String) {
156 | hashtagsRef.queryOrderedByKey().queryStarting(atValue: searchString)
157 | .queryLimited(toFirst: 10).observeSingleEvent(of: .value, with: { snapshot in
158 | let enumerator = snapshot.children
159 | while let hashtag = enumerator.nextObject() as? DataSnapshot {
160 | let tag = hashtag.key
161 | if tag.hasPrefix(searchString) {
162 | self.hashtags.append(tag)
163 | self.collectionView?.insertItems(at: [IndexPath(item: self.hashtags.count - 1, section: 1)])
164 | }
165 | }
166 | })
167 | }
168 |
169 | override func numberOfSections(in collectionView: UICollectionView) -> Int {
170 | return 2
171 | }
172 |
173 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
174 | collectionView.backgroundView = people.isEmpty && hashtags.isEmpty ? emptyLabel : nil
175 | if section == 0 {
176 | return people.count
177 | } else {
178 | return hashtags.count
179 | }
180 | }
181 |
182 | override func collectionView(_ collectionView: UICollectionView,
183 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
184 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
185 | if let cell = cell as? MDCSelfSizingStereoCell {
186 | cell.trailingImageView.isHidden = true
187 | if indexPath.section == 0 {
188 | let user = people[indexPath.item]
189 | if let profilePictureURL = user.profilePictureURL {
190 | UIImage.circleImage(with: profilePictureURL, to: cell.leadingImageView)
191 | } else {
192 | cell.leadingImageView.image = #imageLiteral(resourceName: "ic_account_circle_36pt")
193 | }
194 | cell.titleLabel.text = user.fullname
195 | } else {
196 | cell.leadingImageView.image = #imageLiteral(resourceName: "ic_trending_up")
197 | cell.titleLabel.text = hashtags[indexPath.item]
198 | }
199 | cell.titleLabel.numberOfLines = 1
200 | cell.detailLabel.numberOfLines = 0
201 | }
202 | return cell
203 | }
204 |
205 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
206 | if indexPath.section == 0 {
207 | feedViewController.showProfile(people[indexPath.item])
208 | } else {
209 | feedViewController.showTaggedPhotos(hashtags[indexPath.item])
210 | }
211 | }
212 | }
213 |
214 | extension FPSearchViewController: UISearchResultsUpdating {
215 | func updateSearchResults(for searchController: UISearchController) {
216 | filterContentForSearchText(searchController.searchBar.text!)
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/FriendlyPix/FPUploadViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Firebase
18 | import FirebaseMLVision
19 | import MaterialComponents
20 |
21 | class FPUploadViewController: UIViewController, UITextViewDelegate {
22 | @IBOutlet weak private var imageView: UIImageView!
23 | private var bottomConstraint: NSLayoutConstraint!
24 | private var heightConstraint: NSLayoutConstraint!
25 | private var inputBottomConstraint: NSLayoutConstraint!
26 | private var sendBottomConstraint: NSLayoutConstraint!
27 | private var isKeyboardShown = false
28 | private var mdcSnackBarManager = MDCSnackbarManager()
29 | private let messageInputContainerView: UIView = {
30 | let view = UIView()
31 | view.backgroundColor = .white
32 | return view
33 | }()
34 |
35 | var bottomAreaInset: CGFloat = 0
36 |
37 | let inputTextView: UITextView = {
38 | let textView = UITextView(placeholder: "Write a caption...")
39 | textView.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.callout)
40 | textView.isScrollEnabled = false
41 | textView.returnKeyType = .done
42 | return textView
43 | }()
44 |
45 | let smartTagsView: UIStackView = {
46 | let view = UIStackView()
47 | view.distribution = .equalSpacing
48 | return view
49 | }()
50 |
51 | let sendButton: MDCFloatingButton = {
52 | let button = MDCFloatingButton(shape: .mini)
53 | button.setImage(#imageLiteral(resourceName: "ic_send"), for: .normal)
54 | button.tintColor = .blue
55 | button.backgroundColor = .white
56 | button.accessibilityLabel = "Upload"
57 | button.isEnabled = false
58 | button.addTarget(self, action: #selector(uploadPressed(_:)), for: .touchUpInside)
59 | return button
60 | }()
61 |
62 | var image: UIImage!
63 | var vision: Vision!
64 | @IBOutlet weak private var button: MDCButton!
65 | lazy var database = Database.database()
66 | lazy var storage = Storage.storage()
67 |
68 | let uid = Auth.auth().currentUser!.uid
69 | var fullURL = ""
70 | var thumbURL = ""
71 | var spinner: UIView?
72 |
73 | override func viewDidLoad() {
74 | super.viewDidLoad()
75 | imageView.image = image
76 | detectLabelsInImage()
77 |
78 | if #available(iOS 11.0, *) {
79 | bottomAreaInset = UIApplication.shared.keyWindow!.safeAreaInsets.bottom
80 | }
81 | inputTextView.delegate = self
82 |
83 | NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification),
84 | name: UIResponder.keyboardWillShowNotification, object: nil)
85 | NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification),
86 | name: UIResponder.keyboardWillHideNotification, object: nil)
87 | view.addSubview(messageInputContainerView)
88 |
89 | view.addConstraintsWithFormat(format: "H:|[v0]|", views: messageInputContainerView)
90 |
91 | heightConstraint = messageInputContainerView.heightAnchor.constraint(equalToConstant: 88 + bottomAreaInset)
92 |
93 | bottomConstraint = NSLayoutConstraint(item: messageInputContainerView, attribute: .bottom, relatedBy: .equal,
94 | toItem: view, attribute: .bottom, multiplier: 1, constant: 0)
95 | view.addConstraint(bottomConstraint)
96 | view.addConstraint(heightConstraint)
97 | setupInputComponents()
98 | }
99 |
100 | private func setupInputComponents() {
101 | let topBorderView = UIView()
102 | topBorderView.backgroundColor = UIColor(white: 0.5, alpha: 0.5)
103 | messageInputContainerView.addSubview(inputTextView)
104 | messageInputContainerView.addSubview(sendButton)
105 | messageInputContainerView.addSubview(topBorderView)
106 | messageInputContainerView.addSubview(smartTagsView)
107 |
108 | messageInputContainerView.addConstraintsWithFormat(format: "H:|-8-[v0][v1(40)]-16-|",
109 | views: inputTextView, sendButton)
110 | messageInputContainerView.addConstraintsWithFormat(format: "H:|[v0]|", views: topBorderView)
111 | messageInputContainerView.addConstraintsWithFormat(format: "H:|-16-[v0]-16-|", views: smartTagsView)
112 |
113 | smartTagsView.topAnchor.constraint(equalTo: messageInputContainerView.topAnchor, constant: 6).isActive = true
114 | smartTagsView.bottomAnchor.constraint(equalTo: inputTextView.topAnchor, constant: -6).isActive = true
115 |
116 | inputBottomConstraint = messageInputContainerView.bottomAnchor.constraint(equalTo: inputTextView.bottomAnchor,
117 | constant: bottomAreaInset)
118 | inputBottomConstraint.isActive = true
119 |
120 | sendBottomConstraint = messageInputContainerView.bottomAnchor.constraint(equalTo: sendButton.bottomAnchor,
121 | constant: bottomAreaInset + 6)
122 | sendBottomConstraint.isActive = true
123 |
124 | messageInputContainerView.addConstraintsWithFormat(format: "V:|[v0(0.5)]|", views: topBorderView)
125 | }
126 |
127 | override func viewWillAppear(_ animated: Bool) {
128 | super.viewWillAppear(animated)
129 | inputTextView.becomeFirstResponder()
130 | }
131 |
132 | override func viewWillDisappear(_ animated: Bool) {
133 | super.viewWillDisappear(animated)
134 | if let spinner = spinner {
135 | removeSpinner(spinner)
136 | }
137 | inputTextView.endEditing(true)
138 | }
139 |
140 | func textViewDidEndEditing(_ textView: UITextView) {
141 | sendButton.isEnabled = false
142 | heightConstraint.constant = 88 + bottomAreaInset
143 | }
144 |
145 | func textViewDidChange(_ textView: UITextView) {
146 | sendButton.isEnabled = !textView.text.isEmpty
147 | let size = CGSize(width: view.frame.width - 60, height: .infinity)
148 | let estimatedSize = textView.sizeThatFits(size)
149 | heightConstraint.constant = estimatedSize.height + 54 + (self.isKeyboardShown ? 0 : bottomAreaInset)
150 | }
151 |
152 | func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
153 | if(text == "\n") {
154 | textView.resignFirstResponder()
155 | return true
156 | }
157 | return true
158 | }
159 |
160 | @objc func tagSelected(_ tag: MDCChipView) {
161 | guard let title = tag.titleLabel.text else { return }
162 | inputTextView.insertText(title)
163 | }
164 |
165 | @objc func handleKeyboardNotification(notification: NSNotification) {
166 | guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey]
167 | as? NSValue)?.cgRectValue else { return }
168 | let isKeyboardShowing = notification.name == UIResponder.keyboardWillShowNotification
169 | guard self.isKeyboardShown != isKeyboardShowing else {
170 | bottomConstraint?.constant = isKeyboardShowing ? -keyboardSize.height : 0
171 | return
172 | }
173 | self.isKeyboardShown = isKeyboardShowing
174 | bottomConstraint?.constant = isKeyboardShowing ? -keyboardSize.height : 0
175 | let inset = isKeyboardShowing ? -bottomAreaInset : bottomAreaInset
176 | heightConstraint?.constant += inset
177 | inputBottomConstraint?.constant = isKeyboardShowing ? 0 : bottomAreaInset
178 | sendBottomConstraint?.constant = isKeyboardShowing ? 6 : (6 + bottomAreaInset)
179 | if let animationDuration = notification.userInfo![UIResponder.keyboardAnimationDurationUserInfoKey] as? Double {
180 | UIView.animate(withDuration: animationDuration, delay: 0, options: .curveEaseOut, animations: {
181 | self.view.layoutIfNeeded()
182 | }, completion: nil)
183 | }
184 | }
185 |
186 | func textViewDidBeginEditing(_ textView: UITextView) {
187 | textViewDidChange(textView)
188 | }
189 |
190 |
191 | func detectLabelsInImage() {
192 | self.vision = Vision.vision()
193 | let options = VisionCloudImageLabelerOptions()
194 | options.confidenceThreshold = 0.7
195 | let imageLabeler = vision.cloudImageLabeler(options: options)
196 | let imageMetadata = VisionImageMetadata()
197 | imageMetadata.orientation = FPUploadViewController.visionImageOrientation(from: image.imageOrientation)
198 | let visionImage = VisionImage(image: image)
199 | visionImage.metadata = imageMetadata
200 |
201 | imageLabeler.process(visionImage) { labels, error in
202 | guard error == nil, let labels = labels, !labels.isEmpty else {
203 | return
204 | }
205 |
206 | labels.prefix(3).forEach {
207 | let chip = MDCChipView()
208 | chip.titleLabel.text = "#" + $0.text.components(separatedBy: .whitespaces).joined(separator: "_")
209 | chip.sizeToFit()
210 | chip.addTarget(self, action: #selector(self.tagSelected(_:)), for: .touchUpInside)
211 | self.smartTagsView.addArrangedSubview(chip)
212 | }
213 | }
214 | }
215 |
216 | public static func visionImageOrientation(
217 | from imageOrientation: UIImage.Orientation
218 | ) -> VisionDetectorImageOrientation {
219 | switch imageOrientation {
220 | case .up:
221 | return .topLeft
222 | case .down:
223 | return .bottomRight
224 | case .left:
225 | return .leftBottom
226 | case .right:
227 | return .rightTop
228 | case .upMirrored:
229 | return .topRight
230 | case .downMirrored:
231 | return .bottomLeft
232 | case .leftMirrored:
233 | return .leftTop
234 | case .rightMirrored:
235 | return .rightBottom
236 | }
237 | }
238 |
239 | @IBAction func uploadPressed(_ sender: Any) {
240 | spinner = displaySpinner()
241 | button.isEnabled = false
242 | inputTextView.endEditing(true)
243 | let postRef = database.reference(withPath: "posts").childByAutoId()
244 | guard let postId = postRef.key else { return }
245 | guard let resizedImageData = image.resizeImage(1280, with: 0.9) else { return }
246 | guard let thumbnailImageData = image.resizeImage(640, with: 0.7) else { return }
247 | let fullRef = storage.reference(withPath: "\(self.uid)/full/\(postId)/jpeg")
248 | let thumbRef = storage.reference(withPath: "\(self.uid)/thumb/\(postId)/jpeg")
249 | let metadata = StorageMetadata()
250 | metadata.contentType = "image/jpeg"
251 |
252 | let message = MDCSnackbarMessage()
253 | let myGroup = DispatchGroup()
254 | myGroup.enter()
255 | fullRef.putData(resizedImageData, metadata: metadata) { fullmetadata, error in
256 | if let error = error {
257 | message.text = "Error uploading image"
258 | self.mdcSnackBarManager.show(message)
259 | self.button.isEnabled = true
260 | print("Error uploading image: \(error.localizedDescription)")
261 | return
262 | }
263 |
264 | fullRef.downloadURL(completion: { (url, error) in
265 | if let error = error {
266 | print(error.localizedDescription)
267 | return
268 | }
269 | if let url = url?.absoluteString {
270 | self.fullURL = url
271 | }
272 | myGroup.leave()
273 | })
274 | }
275 | myGroup.enter()
276 | thumbRef.putData(thumbnailImageData, metadata: metadata) { thumbmetadata, error in
277 | if let error = error {
278 | message.text = "Error uploading thumbnail"
279 | self.mdcSnackBarManager.show(message)
280 | self.button.isEnabled = true
281 | print("Error uploading thumbnail: \(error.localizedDescription)")
282 | return
283 | }
284 | thumbRef.downloadURL(completion: { (url, error) in
285 | if let error = error {
286 | print(error.localizedDescription)
287 | return
288 | }
289 | if let url = url?.absoluteString {
290 | self.thumbURL = url
291 | }
292 | myGroup.leave()
293 | })
294 | }
295 | myGroup.notify(queue: .main) {
296 | if let spinner = self.spinner {
297 | self.removeSpinner(spinner)
298 | }
299 |
300 | let trimmedComment = self.inputTextView.text?.trimmingCharacters(in: CharacterSet.whitespaces)
301 | let data = ["full_url": self.fullURL, "full_storage_uri": fullRef.fullPath,
302 | "thumb_url": self.thumbURL, "thumb_storage_uri": thumbRef.fullPath,
303 | "text": trimmedComment ?? "", "client": "ios",
304 | "author": FPUser.currentUser().author(), "timestamp": ServerValue.timestamp()] as [String: Any]
305 | postRef.setValue(data)
306 | postRef.root.updateChildValues(["people/\(self.uid)/posts/\(postId)": true, "feed/\(self.uid)/\(postId)": true])
307 | self.navigationController?.popViewController(animated: true)
308 | }
309 | }
310 | }
311 |
--------------------------------------------------------------------------------
/FriendlyPix/FPUser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Firebase
18 |
19 | class FPUser {
20 | var uid: String
21 | var fullname: String
22 | var profilePictureURL: URL?
23 |
24 | init(snapshot: DataSnapshot) {
25 | self.uid = snapshot.key
26 | let value = snapshot.value as! [String: Any]
27 | self.fullname = value["full_name"] as? String ?? ""
28 | guard let profile_picture = value["profile_picture"] as? String,
29 | let profilePictureURL = URL(string: profile_picture) else { return }
30 | self.profilePictureURL = profilePictureURL
31 | }
32 |
33 | init(dictionary: [String: String]) {
34 | self.uid = dictionary["uid"]!
35 | self.fullname = dictionary["full_name"] ?? ""
36 | guard let profile_picture = dictionary["profile_picture"],
37 | let profilePictureURL = URL(string: profile_picture) else { return }
38 | self.profilePictureURL = profilePictureURL
39 | }
40 |
41 | private init(user: User) {
42 | self.uid = user.uid
43 | self.fullname = user.displayName ?? ""
44 | self.profilePictureURL = user.photoURL
45 | }
46 |
47 | static func currentUser() -> FPUser {
48 | return FPUser(user: Auth.auth().currentUser!)
49 | }
50 |
51 | func author() -> [String: String] {
52 | return ["uid": uid, "full_name": fullname, "profile_picture": profilePictureURL?.absoluteString ?? ""]
53 | }
54 | }
55 |
56 | extension FPUser: Equatable {
57 | static func ==(lhs: FPUser, rhs: FPUser) -> Bool {
58 | return lhs.uid == rhs.uid
59 | }
60 | static func ==(lhs: FPUser, rhs: User) -> Bool {
61 | return lhs.uid == rhs.uid
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/FriendlyPix/FriendlyPix.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 | com.apple.developer.associated-domains
8 |
9 | applinks:z66x3.app.goo.gl
10 |
11 | com.apple.security.application-groups
12 |
13 | group.com.google.common
14 |
15 | keychain-access-groups
16 |
17 | $(AppIdentifierPrefix)com.google.friendlypix.dev
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/FriendlyPix/Launch.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/FriendlyPix/UIImage+Circle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import SDWebImage
18 | import Firebase
19 |
20 | extension UIImage {
21 | var circle: UIImage? {
22 | let square = CGSize(width: min(size.width, size.height), height: min(size.width, size.height))
23 | //let square = CGSize(width: 36, height: 36)
24 | let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: square))
25 | imageView.contentMode = .scaleAspectFill
26 | imageView.image = self
27 | imageView.layer.cornerRadius = square.width / 2
28 | imageView.layer.masksToBounds = true
29 | UIGraphicsBeginImageContext(imageView.bounds.size)
30 | guard let context = UIGraphicsGetCurrentContext() else { return nil }
31 | imageView.layer.render(in: context)
32 | let result = UIGraphicsGetImageFromCurrentImageContext()
33 | UIGraphicsEndImageContext()
34 | return result
35 | }
36 |
37 | func resizeImage(_ dimension: CGFloat) -> UIImage {
38 | var width: CGFloat
39 | var height: CGFloat
40 | var newImage: UIImage
41 |
42 | let size = self.size
43 | let aspectRatio = size.width / size.height
44 |
45 | if aspectRatio > 1 { // Landscape image
46 | width = dimension
47 | height = dimension / aspectRatio
48 | } else { // Portrait image
49 | height = dimension
50 | width = dimension * aspectRatio
51 | }
52 |
53 | if #available(iOS 10.0, *) {
54 | let renderFormat = UIGraphicsImageRendererFormat.default()
55 | let renderer = UIGraphicsImageRenderer(size: CGSize(width: width, height: height), format: renderFormat)
56 | newImage = renderer.image { _ in
57 | self.draw(in: CGRect(x: 0, y: 0, width: width, height: height))
58 | }
59 | } else {
60 | UIGraphicsBeginImageContext(CGSize(width: width, height: height))
61 | self.draw(in: CGRect(x: 0, y: 0, width: width, height: height))
62 | newImage = UIGraphicsGetImageFromCurrentImageContext()!
63 | UIGraphicsEndImageContext()
64 | }
65 | return newImage
66 | }
67 |
68 | func resizeImage(_ dimension: CGFloat, with quality: CGFloat) -> Data? {
69 | return resizeImage(dimension).jpegData(compressionQuality: quality)
70 | }
71 |
72 | static func circleImage(with url: URL, to imageView: UIImageView) {
73 | let urlString = url.absoluteString
74 | let trace = Performance.startTrace(name: "load_profile_pic")
75 | if let image = SDImageCache.shared.imageFromCache(forKey: urlString) {
76 | trace?.incrementMetric("cache", by: 1)
77 | trace?.stop()
78 | imageView.image = image
79 | return
80 | }
81 | SDWebImageDownloader.shared.downloadImage(with: url,
82 | options: .highPriority, progress: nil) { image, _, error, _ in
83 | trace?.incrementMetric("download", by: 1)
84 | trace?.stop()
85 |
86 | if let error = error {
87 | print(error)
88 | return
89 | }
90 | if let image = image {
91 | let circleImage = image.circle
92 | SDImageCache.shared.store(circleImage, forKey: urlString, completion: nil)
93 | imageView.image = circleImage
94 | }
95 | }
96 | }
97 |
98 | static func circleButton(with url: URL, to button: UIBarButtonItem) {
99 | let urlString = url.absoluteString
100 | let trace = Performance.startTrace(name: "load_profile_pic")
101 | if let image = SDImageCache.shared.imageFromCache(forKey: urlString) {
102 | trace?.incrementMetric("cache", by: 1)
103 | trace?.stop()
104 | button.image = image.resizeImage(36).withRenderingMode(.alwaysOriginal)
105 | return
106 | }
107 | SDWebImageDownloader.shared.downloadImage(with: url, options: .highPriority, progress: nil) { image, _, _, _ in
108 | trace?.incrementMetric("download", by: 1)
109 | trace?.stop()
110 | if let image = image {
111 | let circleImage = image.circle
112 | button.tintColor = .red
113 | SDImageCache.shared.store(circleImage, forKey: urlString, completion: nil)
114 | button.image = circleImage?.resizeImage(36).withRenderingMode(.alwaysOriginal)
115 | }
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/FriendlyPix/UITextViewPlaceholder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2017 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import UIKit
18 |
19 | /// Extend UITextView and implemented UITextViewDelegate to listen for changes
20 | extension UITextView {
21 |
22 | public convenience init(placeholder: String) {
23 | self.init()
24 | self.placeholder = placeholder
25 | NotificationCenter.default.addObserver(self,
26 | selector: #selector(textViewDidChange),
27 | name: UITextView.textDidChangeNotification,
28 | object: nil)
29 | }
30 |
31 | /// Resize the placeholder when the UITextView bounds change
32 | override open var bounds: CGRect {
33 | didSet {
34 | self.resizePlaceholder()
35 | }
36 | }
37 |
38 | /// The UITextView placeholder text
39 | public var placeholder: String? {
40 | get {
41 | var placeholderText: String?
42 |
43 | if let placeholderLabel = self.viewWithTag(100) as? UILabel {
44 | placeholderText = placeholderLabel.text
45 | }
46 |
47 | return placeholderText
48 | }
49 | set {
50 | if let placeholderLabel = self.viewWithTag(100) as! UILabel? {
51 | placeholderLabel.text = newValue
52 | placeholderLabel.sizeToFit()
53 | } else {
54 | self.addPlaceholder(newValue!)
55 | }
56 | }
57 | }
58 |
59 | /// When the UITextView did change, show or hide the label based on if the UITextView is empty or not
60 | ///
61 | /// - Parameter textView: The UITextView that got updated
62 | @objc public func textViewDidChange(_ textView: UITextView) {
63 | if let placeholderLabel = self.viewWithTag(100) as? UILabel {
64 | placeholderLabel.isHidden = !self.text.isEmpty
65 | }
66 | }
67 |
68 | /// Resize the placeholder UILabel to make sure it's in the same position as the UITextView text
69 | private func resizePlaceholder() {
70 | if let placeholderLabel = self.viewWithTag(100) as! UILabel? {
71 | let labelX = self.textContainer.lineFragmentPadding
72 | let labelY = self.textContainerInset.top - 2
73 | let labelWidth = self.frame.width - (labelX * 2)
74 | let labelHeight = placeholderLabel.frame.height
75 |
76 | placeholderLabel.frame = CGRect(x: labelX, y: labelY, width: labelWidth, height: labelHeight)
77 | }
78 | }
79 |
80 | /// Adds a placeholder UILabel to this UITextView
81 | private func addPlaceholder(_ placeholderText: String) {
82 | let placeholderLabel = UILabel()
83 |
84 | placeholderLabel.text = placeholderText
85 | placeholderLabel.sizeToFit()
86 |
87 | placeholderLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
88 | placeholderLabel.textColor = UIColor.lightGray
89 | placeholderLabel.tag = 100
90 |
91 | placeholderLabel.isHidden = !self.text.isEmpty
92 |
93 | self.addSubview(placeholderLabel)
94 | self.resizePlaceholder()
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/FriendlyPixTests/FriendlyPixTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2018 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import XCTest
18 |
19 | class FriendlyPixTests: XCTestCase {
20 |
21 | override func setUp() {
22 | super.setUp()
23 | // Put setup code here. This method is called before the invocation of each test method in the class.
24 | }
25 |
26 | override func tearDown() {
27 | // Put teardown code here. This method is called after the invocation of each test method in the class.
28 | super.tearDown()
29 | }
30 |
31 | func testExample() {
32 | // This is an example of a functional test case.
33 | // Use XCTAssert and related functions to verify your tests produce the correct results.
34 | }
35 |
36 | func testPerformanceExample() {
37 | // This is an example of a performance test case.
38 | self.measure {
39 | // Put the code you want to measure the time of here.
40 | }
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/FriendlyPixTests/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 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/FriendlyPixUITests/FriendlyPixUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2018 Google Inc.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import XCTest
18 |
19 | // A simple test of FriendlyPix.
20 | class FriendlyPixUITest: XCTestCase {
21 |
22 | var win: XCUIElement!
23 |
24 | open override func setUp() {
25 | super.setUp()
26 | continueAfterFailure = false
27 | let app = XCUIApplication(bundleIdentifier: "com.google.friendlypix.dev")
28 | app.launch();
29 | win = app.windows.element(boundBy: 0)
30 | }
31 |
32 | func testGuestSignIn() {
33 | acceptAllPermissions()
34 | acceptPrivacyandTerms()
35 | signInAsGuest()
36 | readComment()
37 | }
38 |
39 | func testFriendlyPix() {
40 | acceptAllPermissions()
41 | acceptPrivacyandTerms()
42 | signInWithGoogle()
43 | //takeNewPhoto()
44 | addComment()
45 | }
46 |
47 | func acceptAllPermissions() {
48 | addUIInterruptionMonitor(withDescription: "acceptAllPermissions", handler: {
49 | alert -> Bool in
50 | for id in ["OK", "Allow", "Continue", "Not now"] {
51 | if self.tryToTap(alert.buttons[id]) {
52 | return true
53 | }
54 | }
55 | return false
56 | })
57 | }
58 |
59 | func acceptPrivacyandTerms() {
60 | tryToTap(win.buttons["I agree"], forTimeInterval: 3)
61 | }
62 |
63 | func signInAsGuest() {
64 | let signInButton = win.buttons["Sign in as guest"]
65 | if !signInButton.exists {
66 | signOut()
67 | }
68 | signInButton.tap()
69 | }
70 |
71 | func signOut() {
72 | let profileButton = win.buttons["Profile"]
73 | profileButton.tap()
74 | let logoutButton = win.buttons["Log out"]
75 | if !tryToTap(logoutButton) {
76 | win.buttons["more"].tap()
77 | win.buttons["Sign out"].tap()
78 | }
79 | win.buttons["Logout"].tap()
80 | win.tap()
81 | acceptAllPermissions()
82 | acceptPrivacyandTerms()
83 | }
84 |
85 | func signInWithGoogle() {
86 | let signInButton = win.buttons["Sign in with Google"]
87 | if !signInButton.exists {
88 | signOut()
89 | }
90 | signInButton.tap()
91 |
92 | // Enter email or choose account from account chooser.
93 | let email = ProcessInfo.processInfo.environment["GOOGLE_EMAIL"]!
94 | let emailElement = win
95 | .descendants(matching: .any)
96 | .matching(NSPredicate(format: "(label == 'Email or phone') || (label CONTAINS[c] %@)", email))
97 | .element(boundBy: 0)
98 | if !tryToTap(emailElement, forTimeInterval: 3) {
99 | win.tap() // Sometimes need to trigger the permissions dialog first.
100 | }
101 | if emailElement.exists {
102 | emailElement.tap()
103 | }
104 | if emailElement.exists, emailElement.label == "Email or phone" {
105 | emailElement.tap()
106 | emailElement.typeText(email)
107 | win.tap()
108 | tryToTap(win.buttons["Next"], forTimeInterval: 3)
109 | }
110 |
111 | // Enter the password field if the user is signed out.
112 | let password = ProcessInfo.processInfo.environment["GOOGLE_PASSWORD"]!
113 | let passwordField = win.secureTextFields["Enter your password"]
114 | if tryToTap(passwordField, forTimeInterval: 3) {
115 | passwordField.typeText(password)
116 | win.tap()
117 | tryToTap(win.buttons["Next"], forTimeInterval: 3)
118 | }
119 |
120 | if tryToTap(win.buttons["Confirm your recovery email"]) {
121 | win.tap()
122 | }
123 | }
124 |
125 | func takeNewPhoto() {
126 | win.buttons["Open camera"].forceTapElement()
127 | let takePhotoButton = win.buttons["Take photo"]
128 | if !tryToTap(takePhotoButton, forTimeInterval: 3) {
129 | win.tap()
130 | takePhotoButton.tap()
131 | }
132 | let doneButton = win.buttons["Done"]
133 | if !tryToTap(doneButton, forTimeInterval: 3) {
134 | // The multiple permissions dialogs can prevent the take photo from happening.
135 | takePhotoButton.tap()
136 | if !tryToTap(doneButton, forTimeInterval: 3) {
137 | // No camera
138 | win.buttons["Cancel"].tap()
139 | return
140 | }
141 | }
142 | let captionField = win.textFields.element
143 | XCTAssert(tryToTap(captionField, forTimeInterval: 3))
144 | captionField.typeText("my friendly pic")
145 | win.buttons["UPLOAD THIS PIC"].tap()
146 | }
147 |
148 | func readComment() {
149 | XCTAssert(tryToTap(win.buttons.matching(NSPredicate(format: "label == 'comment'")).element(boundBy: 0), forTimeInterval: 3))
150 | let commentField = win.textViews.element
151 | commentField.tap()
152 | XCTAssert(!XCUIApplication().keyboards.element.exists)
153 | }
154 |
155 | func addComment() {
156 | XCTAssert(tryToTap(win.buttons.matching(NSPredicate(format: "label == 'comment'")).element(boundBy: 0), forTimeInterval: 3))
157 | let commentField = win.textViews.element
158 | commentField.tap()
159 | commentField.typeText("Nice, pic!")
160 | win.buttons["Send comment"].tap()
161 | win.buttons["Back"].tap()
162 | }
163 |
164 | func tryToTap(_ element: XCUIElement, forTimeInterval ti: Double = 0) -> Bool {
165 | let startTime = NSDate.timeIntervalSinceReferenceDate
166 | repeat {
167 | if element.exists && element.isHittable {
168 | element.tap()
169 | return true
170 | }
171 | } while NSDate.timeIntervalSinceReferenceDate - startTime < ti
172 | return false
173 | }
174 |
175 | func wait(forElement element: XCUIElement, timeout: TimeInterval) {
176 | let predicate = NSPredicate(format: "exists == 1")
177 |
178 | // This will make the test runner continously evalulate the
179 | // predicate, and wait until it matches.
180 | expectation(for: predicate, evaluatedWith: element)
181 | waitForExpectations(timeout: timeout)
182 | }
183 | }
184 |
185 | extension XCUIElement {
186 | func forceTapElement() {
187 | if self.isHittable {
188 | self.tap()
189 | }
190 | else {
191 | let coordinate: XCUICoordinate = self.coordinate(withNormalizedOffset: CGVector(dx:0.0, dy:0.0))
192 | coordinate.tap()
193 | }
194 | }
195 | }
196 |
197 |
--------------------------------------------------------------------------------
/FriendlyPixUITests/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 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'cocoapods', :git => 'https://github.com/CocoaPods/CocoaPods.git'
4 | gem 'cocoapods-core', :git => 'https://github.com/CocoaPods/Core.git'
5 | gem 'xcodeproj', :git => 'https://github.com/CocoaPods/Xcodeproj.git'
6 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: https://github.com/CocoaPods/CocoaPods.git
3 | revision: f5ea2cf05f562428830d7d9288020c01f46628bc
4 | specs:
5 | cocoapods (1.11.3)
6 | addressable (~> 2.8)
7 | claide (>= 1.0.2, < 2.0)
8 | cocoapods-core (= 1.11.3)
9 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
10 | cocoapods-downloader (>= 1.6.0, < 2.0)
11 | cocoapods-plugins (>= 1.0.0, < 2.0)
12 | cocoapods-search (>= 1.0.0, < 2.0)
13 | cocoapods-trunk (>= 1.6.0, < 2.0)
14 | cocoapods-try (>= 1.1.0, < 2.0)
15 | colored2 (~> 3.1)
16 | escape (~> 0.0.4)
17 | fourflusher (>= 2.3.0, < 3.0)
18 | gh_inspector (~> 1.0)
19 | molinillo (~> 0.8.0)
20 | nap (~> 1.0)
21 | ruby-macho (>= 2.3.0, < 3.0)
22 | xcodeproj (>= 1.21.0, < 2.0)
23 |
24 | GIT
25 | remote: https://github.com/CocoaPods/Core.git
26 | revision: c6ac388ee43f0782fdb8e32386f41f96dbe29082
27 | specs:
28 | cocoapods-core (1.11.3)
29 | activesupport (>= 5.0, < 7)
30 | addressable (~> 2.8)
31 | algoliasearch (~> 1.0)
32 | concurrent-ruby (~> 1.1)
33 | fuzzy_match (~> 2.0.4)
34 | nap (~> 1.0)
35 | netrc (~> 0.11)
36 | public_suffix (~> 4.0)
37 | typhoeus (~> 1.0)
38 |
39 | GIT
40 | remote: https://github.com/CocoaPods/Xcodeproj.git
41 | revision: 29cd0821d47f864abbd1ca80f23ff2aded0adfed
42 | specs:
43 | xcodeproj (1.22.0)
44 | CFPropertyList (>= 2.3.3, < 4.0)
45 | atomos (~> 0.1.3)
46 | claide (>= 1.0.2, < 2.0)
47 | colored2 (~> 3.1)
48 | nanaimo (~> 0.3.0)
49 | rexml (~> 3.2.4)
50 |
51 | GEM
52 | remote: https://rubygems.org/
53 | specs:
54 | CFPropertyList (3.0.5)
55 | rexml
56 | activesupport (6.1.7)
57 | concurrent-ruby (~> 1.0, >= 1.0.2)
58 | i18n (>= 1.6, < 2)
59 | minitest (>= 5.1)
60 | tzinfo (~> 2.0)
61 | zeitwerk (~> 2.3)
62 | addressable (2.8.1)
63 | public_suffix (>= 2.0.2, < 6.0)
64 | algoliasearch (1.27.5)
65 | httpclient (~> 2.8, >= 2.8.3)
66 | json (>= 1.5.1)
67 | atomos (0.1.3)
68 | claide (1.1.0)
69 | cocoapods-deintegrate (1.0.5)
70 | cocoapods-downloader (1.6.3)
71 | cocoapods-plugins (1.0.0)
72 | nap
73 | cocoapods-search (1.0.1)
74 | cocoapods-trunk (1.6.0)
75 | nap (>= 0.8, < 2.0)
76 | netrc (~> 0.11)
77 | cocoapods-try (1.2.0)
78 | colored2 (3.1.2)
79 | concurrent-ruby (1.1.10)
80 | escape (0.0.4)
81 | ethon (0.16.0)
82 | ffi (>= 1.15.0)
83 | ffi (1.15.5)
84 | fourflusher (2.3.1)
85 | fuzzy_match (2.0.4)
86 | gh_inspector (1.1.3)
87 | httpclient (2.8.3)
88 | i18n (1.12.0)
89 | concurrent-ruby (~> 1.0)
90 | json (2.6.2)
91 | minitest (5.16.3)
92 | molinillo (0.8.0)
93 | nanaimo (0.3.0)
94 | nap (1.1.0)
95 | netrc (0.11.0)
96 | public_suffix (4.0.7)
97 | rexml (3.2.5)
98 | ruby-macho (2.5.1)
99 | typhoeus (1.4.0)
100 | ethon (>= 0.9.0)
101 | tzinfo (2.0.5)
102 | concurrent-ruby (~> 1.0)
103 | zeitwerk (2.6.6)
104 |
105 | PLATFORMS
106 | ruby
107 |
108 | DEPENDENCIES
109 | cocoapods!
110 | cocoapods-core!
111 | xcodeproj!
112 |
113 | BUNDLED WITH
114 | 2.3.25
115 |
--------------------------------------------------------------------------------
/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIcons
10 |
11 | CFBundleIcons~ipad
12 |
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | $(PRODUCT_NAME)
19 | CFBundlePackageType
20 | APPL
21 | CFBundleShortVersionString
22 | 0.2.4
23 | CFBundleURLTypes
24 |
25 |
26 | CFBundleTypeRole
27 | Editor
28 | CFBundleURLName
29 | google
30 | CFBundleURLSchemes
31 |
32 | REVERSED_CLIENT_ID
33 |
34 |
35 |
36 | CFBundleVersion
37 | 0
38 | ITSAppUsesNonExemptEncryption
39 |
40 | LSApplicationCategoryType
41 |
42 | LSRequiresIPhoneOS
43 |
44 | NSAppTransportSecurity
45 |
46 | NSAllowsArbitraryLoads
47 |
48 |
49 | NSCameraUsageDescription
50 | to take photos to upload.
51 | NSPhotoLibraryUsageDescription
52 | to present photos to upload.
53 | UIBackgroundModes
54 |
55 | remote-notification
56 |
57 | UILaunchStoryboardName
58 | Launch
59 | UIMainStoryboardFile
60 | Main
61 | UIRequiredDeviceCapabilities
62 |
63 | armv7
64 |
65 | UIStatusBarTintParameters
66 |
67 | UINavigationBar
68 |
69 | Style
70 | UIBarStyleDefault
71 | Translucent
72 |
73 |
74 |
75 | UISupportedInterfaceOrientations
76 |
77 | UIInterfaceOrientationPortrait
78 | UIInterfaceOrientationLandscapeLeft
79 | UIInterfaceOrientationLandscapeRight
80 |
81 | UISupportedInterfaceOrientations~ipad
82 |
83 | UIInterfaceOrientationPortrait
84 | UIInterfaceOrientationPortraitUpsideDown
85 | UIInterfaceOrientationLandscapeLeft
86 | UIInterfaceOrientationLandscapeRight
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2015 Google Inc
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
204 | All code in any directories or sub-directories that end with *.html or
205 | *.css is licensed under the Creative Commons Attribution International
206 | 4.0 License, which full text can be found here:
207 | https://creativecommons.org/licenses/by/4.0/legalcode.
208 |
209 | As an exception to this license, all html or css that is generated by
210 | the software at the direction of the user is copyright the user. The
211 | user has full ownership and control over such content, including
212 | whether and how they wish to license it.
213 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | platform :ios, '13.0'
2 | target 'FriendlyPix' do
3 | use_frameworks!
4 |
5 | pod 'Firebase/Analytics'
6 | pod 'Firebase/Auth'
7 | pod 'Firebase/Crashlytics'
8 | pod 'Firebase/Database'
9 | pod 'Firebase/Messaging'
10 | pod 'Firebase/MLVision'
11 | pod 'Firebase/Performance'
12 | pod 'Firebase/Storage'
13 | pod 'FirebaseUI/Anonymous'
14 | pod 'FirebaseUI/Auth'
15 | pod 'FirebaseUI/Google'
16 | pod 'ImagePicker'
17 | pod 'Lightbox'
18 | pod 'MaterialComponents/BottomAppBar'
19 | pod 'MaterialComponents/Buttons+ColorThemer'
20 | pod 'MaterialComponents/Cards'
21 | pod 'MaterialComponents/Chips'
22 | pod 'MaterialComponents/Dialogs'
23 | pod 'MaterialComponents/Dialogs+ColorThemer'
24 | pod 'MaterialComponents/Snackbar'
25 | pod 'MaterialComponents/TextFields'
26 | pod 'MaterialComponents/List'
27 | pod 'SDWebImage'
28 | end
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Friendly Pix iOS
2 |
3 | FriendlyPix is a simple app to capture and share your favorite moments. It demonstrates the best practises of building an iOS app on the Firebase Platform. Follow interesting accounts of your choice. Interact with them through the comments. Stay up-to-date with the latest photos posted in the community.
4 |
5 | Use FriendlyPix to:
6 |
7 | * Post photos you want to keep on your profile grid.
8 | * Search profiles for friends and family.
9 | * Follow accounts to add photos from them in your Home feed.
10 | * Explore the latest photos from the Trending feed.
11 | * Interact with community through the comments under each photo.
12 |
13 |
14 |
15 | ## Initial setup, build tools and dependencies
16 |
17 | Friendly Pix iOS is built using Swift and [Firebase](https://firebase.google.com/docs/ios/setup). The Auth flow is built using [Firebase-UI](https://github.com/firebase/firebaseui-ios). Dependencies are managed using [CocoaPods](https://cocoapods.org/). Additionally server-side micro-services are built on [Cloud Functions for Firebase](https://firebase.google.com/docs/functions).
18 |
19 | Simply install the pods and open the .xcworkspace file to see the project in Xcode.
20 |
21 | ```
22 | $ pod install
23 | $ open your-project.xcworkspace
24 | ```
25 |
26 | ## Create Firebase Project
27 |
28 | 1. Create a Firebase project using the [Firebase Console](https://firebase.google.com/console).
29 | 1. To add the FriendlyPix app to a Firebase project, use the bundleID `com.google.firebase.friendlypix`.
30 | 1. Download the generated `GoogleService-Info.plist` file, and copy it to the root directory of this app.
31 |
32 | ### Google Sign In Setup
33 | - Go to the [Firebase Console](https://console.firebase.google.com) and navigate to your project:
34 | - Select the **Auth** panel and then click the **Sign In Method** tab.
35 | - Click **Google** and turn on the **Enable** switch, then click **Save**.
36 | - Open your regular `Info.plist`, navigate to `URL types > Item 0 > URL schemes`, and replace the value
37 | of `YOUR_REVERSED_CLIENT_ID` with the value of `REVERSED_CLIENT_ID` from the GoogleService-Info.plist`.
38 | - Run the app on your device or simulator.
39 | - Select **Sign In** and select Google to begin.
40 |
41 | ### Facebook Login Setup
42 | - Go to the [Facebook Developers Site](https://developers.facebook.com) and follow all
43 | instructions to set up a new iOS app. When asked for a bundle ID, use
44 | `com.google.firebase.quickstart.AuthenticationExample`.
45 | - Go to the [Firebase Console](https://console.firebase.google.com) and navigate to your project:
46 | - Select the **Auth** panel and then click the **Sign In Method** tab.
47 | - Click **Facebook** and turn on the **Enable** switch, then click **Save**.
48 | - Enter your Facebook **App Id** and **App Secret** and click **Save**.
49 | - Open your regular `Info.plist` and replace the value of the `FacebookAppID` with the ID of the
50 | Facebook app you just created, e.g 124567. Save that file.
51 | - In the *Info* tab of your target settings add a *URL Type* with a *URL Scheme* of 'fb' + the ID
52 | of your Facebook app, e.g. fb1234567.
53 | - Run the app on your device or simulator.
54 | - Select **Sign In** and select Facebook to begin.
55 |
56 |
57 | ## Requirements
58 |
59 | The mobile FriendlyPix app need the Cloud Functions, the Realtime Database rules and the Cloud Storage rules to be deployed to work properly. You can find instructions at [FriendlyPix Web Repository](https://github.com/firebase/friendlypix-web/blob/master/README.md#mobile-apps).
60 |
61 |
62 | ## Contributing
63 |
64 | We'd love that you contribute to the project. Before doing so please read our [Contributor guide](../CONTRIBUTING.md).
65 |
66 |
67 | ## License
68 |
69 | © Google, 2011. Licensed under an [Apache-2](../LICENSE) license.
70 |
--------------------------------------------------------------------------------
/friendly-pix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/friendly-pix.png
--------------------------------------------------------------------------------
/info_script.rb:
--------------------------------------------------------------------------------
1 | require 'xcodeproj'
2 | project_path = "FriendlyPix.xcodeproj"
3 | project = Xcodeproj::Project.open(project_path)
4 |
5 | # Add a file to the project in the main group
6 | file_name = 'GoogleService-Info.plist'
7 | file = project.new_file(file_name)
8 |
9 | # Add the file to the all targets
10 | project.targets.each do |target|
11 | target.add_file_references([file])
12 | end
13 |
14 | #save project
15 | project.save()
16 |
--------------------------------------------------------------------------------
/mock-GoogleService-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AD_UNIT_ID_FOR_BANNER_TEST
6 | ca-app-pub-3940256099942544/2934735716
7 | AD_UNIT_ID_FOR_INTERSTITIAL_TEST
8 | ca-app-pub-3940256099942544/4411468910
9 | API_KEY
10 | AIzaSyAzlj4APqi5S58nFtE52Da-fYBOHA2MhaY
11 | com.google.friendlypix.dev
12 | id
13 | CLIENT_ID
14 | 123456789000-hjugbg6ud799v4c49dim8ce2usclthar.apps.googleusercontent.com
15 | DATABASE_URL
16 | https://mockproject-1234.firebaseio.com
17 | GCM_SENDER_ID
18 | 123456789000
19 | GOOGLE_APP_ID
20 | 1:123456789000:ios:f1bf012572b04063
21 | IS_ADS_ENABLED
22 |
23 | IS_ANALYTICS_ENABLED
24 |
25 | IS_APPINVITE_ENABLED
26 |
27 | IS_GCM_ENABLED
28 |
29 | IS_SIGNIN_ENABLED
30 |
31 | PLIST_VERSION
32 | 1
33 | PROJECT_ID
34 | mockproject-1234
35 | REVERSED_CLIENT_ID
36 | com.googleusercontent.apps.123456789000-hjugbg6ud799v4c49dim8ce2usclthar
37 | STORAGE_BUCKET
38 | mockproject-1234.appspot.com
39 |
40 |
41 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -eo pipefail
4 |
5 | EXIT_STATUS=0
6 |
7 | (xcodebuild \
8 | -workspace FriendlyPix.xcworkspace \
9 | -scheme FriendlyPix \
10 | -sdk iphonesimulator \
11 | -destination 'platform=iOS Simulator,name=iPhone XR' \
12 | build \
13 | #test \
14 | ONLY_ACTIVE_ARCH=YES \
15 | CODE_SIGNING_REQUIRED=NO \
16 | | xcpretty) || EXIT_STATUS=$?
17 |
18 | exit $EXIT_STATUS
19 |
--------------------------------------------------------------------------------